您当前的位置:首页 > 电脑百科 > 程序开发 > 架构

这可能是讲分布式系统最到位的一篇文章

时间:2020-01-26 16:58:34  来源:  作者:

这可能是讲分布式系统最到位的一篇文章

 

一个网站就是一个应用,当系统压力较大时,只能横向扩展,增加多个服务器或者多个容器去做负载均衡,避免单点故障而影响到整个系统。

 

集中式最明显的优点就是开发,测试,运维会比较方便,不用考虑复杂的分布式环境。

 

弊端也很明显,系统大而复杂、不易扩展、难于维护,每次更新都必须更新所有的应用。

这可能是讲分布式系统最到位的一篇文章

 

集中式系统拓扑图

 

鉴于集中式系统的种种弊端,促成了分布式系统的形成,分布式系统背后是由一系列的计算机组成,但用户感知不到背后的逻辑,就像访问单个计算机一样,天然的避免了单机故障的问题。

 

应用可以按业务类型拆分成多个应用或服务,再按结构分成接口层、服务层。

 

我们也可以按访问入口分,如移动端、PC 端等定义不同的接口应用。数据库可以按业务类型拆分成多个实例,还可以对单表进行分库分表。同时增加分布式缓存、消息队列、非关系型数据库、搜索等中间件。

 

分布式系统虽好,但是增加了系统的复杂性,如分布式事务、分布式锁、分布式 Session、数据一致性等都是现在分布式系统中需要解决的难题。

 

分布式系统也增加了开发测试运维的成本,工作量增加,其管理不好反而会变成一种负担。

这可能是讲分布式系统最到位的一篇文章

 

分布式系统拓扑图

 

分布式系统最为核心的要属分布式服务框架,有了分布式服务框架,我们只需关注各自的业务,而无需去关注那些复杂的服务之间调用的过程。

 

分布式服务框架

 

目前业界比较流行的分布式服务框架有:阿里的 Dubbo、Spring Cloud。

 

这里不对这些分布式服务框架做对比,简单的说说他们都做了些什么,能使我们用远程服务就像调用本地服务那么简单高效。

 

服务

 

服务是对使用用户有功能输出的模块,以技术框架作为基础,能实现用户的需求。

 

比如日志记录服务、权限管理服务、后台服务、配置服务、缓存服务、存储服务、消息服务等,这些服务可以灵活的组合在一起,也可以独立运行。

 

服务需要有接口,与系统进行对接。面向服务的开发,应该是把服务拆分开发,把服务组合运行。

 

更加直接的例子如:历史详情、留言板、评论、评级服务等。他们之间能独立运行,也要能组合在一起作为一个整体。

 

注册中心

 

注册中心对整个分布式系统起着最为核心的整合作用,支持对等集群,需要提供 CRUD 接口,支持订阅发布机制且可靠性要求非常之高,一般拿 Zookeeper 集群来做为注册中心。

 

分布式环境中服务提供方的服务会在多台服务器上部署,每台服务器会向注册中心提供服务方标识、服务列表、地址、对应端口、序列化协议等信息。

 

注册中心记录下服务和服务地址的映射关系,一般一个服务会对应多个地址,这个过程我们称之为服务发布或服务注册。

 

服务调用方会根据服务方标识、服务列表从注册中心获取所需服务的信息(地址端口信息、序列化协议等),这些信息会缓存至本地。

 

当服务需要调用其他服务时,直接在这里找到服务的地址,进行调用,这个过程我们称之为服务发现。

这可能是讲分布式系统最到位的一篇文章

 

注册中心

 

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

/***创建node节点*@paramnode*@paramdata*/publicbooleancreateNode(Stringnode,Stringdata){try{byte[]bytes=data.getBytes();//同步创建临时顺序节点Stringpath=zk.create(ZkConstant.ZK_RPC_DATA_PATH+"/"+node+"-",bytes,ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.EPHEMERAL_SEQUENTIAL);log.info("createzookeepernode({}=>{})",path,data);}catch(KeeperExceptione){log.error("",e);returnfalse;}catch(InterruptedExceptionex){log.error("",ex);returnfalse;}returntrue;}

 

这可能是讲分布式系统最到位的一篇文章

 

子节点 1

这可能是讲分布式系统最到位的一篇文章

 

子节点 2

 

如下面 Zookeeper 中写入的临时顺序节点信息:

  • com.black.blackrpc.test.Helloword发布服务时对外的名称。
  • 00000000010,00000000011:ZK 顺序节点 ID。
  • 127.0.0.1:8888,127.0.0.1:8889:服务地址端口。
  • Protostuff:序列化方式。
  • 1.0:权值,负载均衡策略使用。

 

这里使用的是 Zookeeper 的临时顺序节点,为什么使用临时顺序节点,主要是考虑以下两点:

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

 

下面是 ZK 的数据同步过程:

/***同步节点(通知模式)*syncNodes会通过级联方式,在每次watcher被触发后,就会再挂上新的watcher。完成了类似链式触发的功能*/publicbooleansyncNodes(){try{List<String>nodeList=zk.getChildren(ZkConstant.ZK_RPC_DATA_PATH,newWatcher(){@Overridepublicvoidprocess(WatchedEventevent){if(event.getType()==Event.EventType.NodeChildrenChanged){syncNodes();}}});Map<String,List<String>>map=newHashMap<String,List<String>>();for(Stringnode:nodeList){byte[]bytes=zk.getData(ZkConstant.ZK_RPC_DATA_PATH+"/"+node,false,null);Stringkey=node.substring(0,node.lastIndexOf(ZkConstant.DELIMITED_MARKER));Stringvalue=newString(bytes);Objectobject=map.get(key);if(object!=null){((List<String>)object).add(value);}else{List<String>dataList=newArrayList<String>();dataList.add(value);map.put(key,dataList);}log.info("node:[{}]data:[{}]",node,newString(bytes));}/**修改连接的地址缓存*/if(MapUtil.isNotEmpty(map)){log.debug("invokingservicecacheupdateing....");InvokingServiceCache.updataInvokingServiceMap(map);}returntrue;}catch(KeeperException|InterruptedExceptione){log.error(e.toString());returnfalse;}}

 

当数据同步到本地时,一般会写入到本地文件中,防止因 Zookeeper 集群异常下线而无法获取服务提供者信息。

 

通讯与协议

 

服务消费者无论是与注册中心还是与服务提供者,都需要存在网络连接传输数据,而这就涉及到通讯。

 

笔者之前也做过这方面的工作,当时使用的是 JAVA BIO 简单的写了一个通讯包,使用场景没有多大的并发,阻塞式的 BIO 也未暴露太多问题。

 

java BIO 因其建立连接之后会阻塞线程等待数据,这种方式必须以一连接一线程的方式,即客户端有连接请求时服务器端就需要启动一个线程进行处理。当连接数过大时,会建立相当多的线程,性能直线下降。

 

Java NIO:同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求时才启动一个线程进行处理。

 

Java AIO:异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的 I/O 请求都是由 OS 先完成了再通知服务器应用去启动线程进行处理。

 

BIO、NIO、AIO 适用场景分析:

  • BIO:用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,但程序直观简单易理解。
  • NIO:适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂。目前主流的通讯框架 Netty、Apache Mina、Grizzl、NIO Framework 都是基于其实现的。
  • AIO:用于连接数目多且连接比较长(重操作)的架构,比如图片服务器,文件传输等,充分调用 OS 参与并发操作,编程比较复杂。

 

作为基石的通讯,其实要考虑很多东西。如:丢包粘包的情况,心跳机制,断连重连,消息缓存重发,资源的优雅释放,长连接还是短连接等。

 

下面是 Netty 建立服务端,客户端的简单实现:

importio.netty.bootstrap.ServerBootstrap;importio.netty.channel.ChannelInitializer;importio.netty.channel.ChannelPipeline;importio.netty.channel.EventLoopGroup;importio.netty.channel.nio.NioEventLoopGroup;importio.netty.channel.socket.SocketChannel;importio.netty.channel.socket.nio.NIOServerSocketChannel;importio.netty.handler.codec.LengthFieldBasedFrameDecoder;importio.netty.handler.codec.LengthFieldPrepender;importio.netty.handler.codec.bytes.ByteArrayDecoder;importio.netty.handler.codec.bytes.ByteArrayEncoder;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;/****nettytcp服务端*@authorv_wangshiyu**/publicclassNettyTcpService{privatestaticfinalLoggerlog=LoggerFactory.getLogger(NettyTcpService.class);privateStringhost;privateintport;publicNettyTcpService(Stringaddress)throwsException{Stringstr[]=address.split(":");this.host=str[0];this.port=Integer.valueOf(str[1]);}publicNettyTcpService(Stringhost,intport)throwsException{this.host=host;this.port=port;}/**用于分配处理业务线程的线程组个数*/privatestaticfinalintBIZGROUPSIZE=Runtime.getRuntime().availableProcessors()*2;//默认/**业务出现线程大小*/privatestaticfinalintBIZTHREADSIZE=4;/**NioEventLoopGroup实际上就是个线程,*NioEventLoopGroup在后台启动了n个NioEventLoop来处理Channel事件,*每一个NioEventLoop负责处理m个Channel,*NioEventLoopGroup从NioEventLoop数组里挨个取出NioEventLoop来处理Channel*/privatestaticfinalEventLoopGroupbossGroup=newNioEventLoopGroup(BIZGROUPSIZE);privatestaticfinalEventLoopGroupworkerGroup=newNioEventLoopGroup(BIZTHREADSIZE);publicvoidstart()throwsException{log.info("NettyTcpServiceRun...");ServerBootstrapb=newServerBootstrap();b.group(bossGroup,workerGroup);b.channel(NioServerSocketChannel.class);b.childHandler(newChannelInitializer<SocketChannel>(){@OverridepublicvoidinitChannel(SocketChannelch)throwsException{ChannelPipelinepipeline=ch.pipeline();pipeline.addLast("frameDecoder",newLengthFieldBasedFrameDecoder(Integer.MAX_VALUE,0,4,0,4));pipeline.addLast("frameEncoder",newLengthFieldPrepender(4));pipeline.addLast("decoder",newByteArrayDecoder());pipeline.addLast("encoder",newByteArrayEncoder());//pipeline.addLast(newEncoder());//pipeline.addLast(newDecoder());pipeline.addLast(newTcpServerHandler());}});b.bind(host,port).sync();log.info("NettyTcpServiceSuccess!");}/***停止服务并释放资源*/publicvoidshutdown(){workerGroup.shutdownGracefully();bossGroup.shutdownGracefully();}}importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importio.netty.channel.ChannelHandlerContext;importio.netty.channel.SimpleChannelInboundHandler;/***服务端处理器*/publicclassTcpServerHandlerextendsSimpleChannelInboundHandler<Object>{privatestaticfinalLoggerlog=LoggerFactory.getLogger(TcpServerHandler.class);@OverrideprotectedvoidchannelRead0(ChannelHandlerContextctx,Objectmsg)throwsException{byte[]data=(byte[])msg;}}

 

importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importio.netty.bootstrap.Bootstrap;importio.netty.channel.Channel;importio.netty.channel.ChannelInitializer;importio.netty.channel.ChannelOption;importio.netty.channel.ChannelPipeline;importio.netty.channel.EventLoopGroup;importio.netty.channel.nio.NioEventLoopGroup;importio.netty.channel.socket.nio.NioSocketChannel;importio.netty.handler.codec.LengthFieldBasedFrameDecoder;importio.netty.handler.codec.LengthFieldPrepender;importio.netty.handler.codec.bytes.ByteArrayDecoder;importio.netty.handler.codec.bytes.ByteArrayEncoder;importio.netty.util.concurrent.Future;/***nettytcp客户端*@authorv_wangshiyu**/publicclassNettyTcpClient{privatestaticfinalLoggerlog=LoggerFactory.getLogger(NettyTcpClient.class);privateStringhost;privateintport;privateBootstrapbootstrap;privateChannelchannel;privateEventLoopGroupgroup;publicNettyTcpClient(Stringhost,intport){bootstrap=getBootstrap();channel=getChannel(host,port);this.host=host;this.port=port;}publicStringgetHost(){returnhost;}publicintgetPort(){returnport;}/***初始化Bootstrap*@return*/publicfinalBootstrapgetBootstrap(){group=newNioEventLoopGroup();Bootstrapb=newBootstrap();b.group(group).channel(NioSocketChannel.class);b.handler(newChannelInitializer<Channel>(){@OverrideprotectedvoidinitChannel(Channelch)throwsException{ChannelPipelinepipeline=ch.pipeline();//pipeline.addLast(newEncoder());//pipeline.addLast(newDecoder());pipeline.addLast("frameDecoder",newLengthFieldBasedFrameDecoder(Integer.MAX_VALUE,0,4,0,4));pipeline.addLast("frameEncoder",newLengthFieldPrepender(4));pipeline.addLast("decoder",newByteArrayDecoder());pipeline.addLast("encoder",newByteArrayEncoder());pipeline.addLast("handler",newTcpClientHandler());}});b.option(ChannelOption.SO_KEEPALIVE,true);returnb;}/***连接,获取Channel*@paramhost*@paramport*@return*/publicfinalChannelgetChannel(Stringhost,intport){Channelchannel=null;try{channel=bootstrap.connect(host,port).sync().channel();returnchannel;}catch(Exceptione){log.info(String.format("connectServer(IP[%s],PORT[%s])fail!",host,port));returnnull;}}/***发送消息*@parammsg*@throwsException*/publicbooleansendMsg(Objectmsg)throwsException{if(channel!=null){channel.writeAndFlush(msg).sync();log.debug("msgflushsuccess");returntrue;}else{log.debug("msgflushfail,connectisnull");returnfalse;}}/***连接断开*并且释放资源*@return*/publicbooleandisconnectConnect(){//channel.close().awaitUninterruptibly();Future<?>future=group.shutdownGracefully();//shutdownGracefully释放所有资源,并且关闭所有当前正在使用的channelfuture.syncUninterruptibly();returntrue;}}importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importio.netty.channel.ChannelHandlerContext;importio.netty.channel.SimpleChannelInboundHandler;/***客户端处理器*/publicclassTcpClientHandlerextendsSimpleChannelInboundHandler<Object>{privatestaticfinalLoggerlog=LoggerFactory.getLogger(TcpClientHandler.class);@OverrideprotectedvoidchannelRead0(ChannelHandlerContextctx,Objectmsg)throwsException{byte[]data=(byte[])msg;}}

 

说到通讯就不能不说协议,通信时所遵守的规则,访问什么,传输的格式等都属于协议。

 

作为一个开发人员,应该都了解 TCP/IP 协议,它是一个网络通信模型,以及一整套网络传输协议家族,是互联网的基础通信架构。

 

也都应该用过 Http(超文本传输协议),Web 服务器传输超文本到本地浏览器的传送协议,该协议建立在 TCP/IP 协议之上。分布式服务框架服务间的调用也会规定协议。

 

为了支持不同场景,分布式服务框架会存在多种协议,如 Dubbo 就支持 7 种协议:Dubbo 协议(默认),RMI 协议,Hessian协议,HTTP 协议,WebService 协议,Thrift 协议,Memcached 协议,redis 协议每种协议应对的场景不尽相同,具体场景具体对待。

 

服务路由

 

分布式服务上线时都是集群组网部署,集群中会存在某个服务的多实例,消费者如何从服务列表中选择合适的服务提供者进行调用,这就涉及到服务路由。分布式服务框架需要能够满足用户灵活的路由需求。

 

透明化路由

 

很多开源的 RPC 框架调用者需要配置服务提供者的地址信息,尽管可以通过读取数据库的服务地址列表等方式避免硬编码地址信息,但是消费者依然要感知服务提供者的地址信息,这违反了透明化路由原则。

 

基于服务注册中心的服务订阅发布,消费者通过主动查询和被动通知的方式获取服务提供者的地址信息,而不再需要通过硬编码方式得到提供者的地址信息。

 

只需要知道当前系统发布了那些服务,而不需要知道服务具体存在于什么位置,这就是透明化路由。  

 

负载均衡

 

负载均衡策略是服务的重要属性,分布式服务框架通常会提供多种负载均衡策略,同时支持用户扩展负载均衡策略。

 

随机

 

通常在对等集群组网中,采用随机算法进行负载均衡,随机路由算法消息分发还是比较均匀的,采用 JDK 提供的 java.util.Random 或者 java.security.SecureRandom 在指定服务提供者列表中生成随机地址。

 

消费者基于随机生成的服务提供者地址进行远程调用:

/***随机*/publicclassRandomStrategyimplementsClusterStrategy{@OverridepublicRemoteServiceBaseselect(List<RemoteServiceBase>list){intMAX_LEN=list.size();intindex=RandomUtil.nextInt(MAX_LEN);returnlist.get(index);}}

 

随机还是存在缺点的,可能出现部分节点的碰撞的概率较高,另外硬件配置差异较大时,会导致各节点负载不均匀。

 

为避免这些问题,需要对服务列表加权,性能好的机器接收的请求的概率应该高于一般机器:

/***加权随机*/publicclassWeightingRandomStrategyimplementsClusterStrategy{@OverridepublicRemoteServiceBaseselect(List<RemoteServiceBase>list){//存放加权后的服务提供者列表List<RemoteServiceBase>weightingList=newArrayList<RemoteServiceBase>();for(RemoteServiceBaseremoteServiceBase:list){//扩大10倍intweight=(int)(remoteServiceBase.getWeight()*10);for(inti=0;i<weight;i++){weightingList.add(remoteServiceBase);}}intMAX_LEN=weightingList.size();intindex=RandomUtil.nextInt(MAX_LEN);returnweightingList.get(index);}}

 

轮询

 

逐个请求服务地址,到达边界之后,继续绕接。主要缺点:慢的提供者会累积请求。

 

例如第二台机器很慢,但没挂。当请求第二台机器时被卡在那。久而久之,所有请求都卡在第二台机器上。

 

轮询策略实现非常简单,顺序循环遍历服务提供者列表,达到边界之后重新归零开始,继续顺序循环:

/***轮询*/publicclassPollingStrategyimplementsClusterStrategy{//计数器privateintindex=0;privateLocklock=newReentrantLock();@OverridepublicRemoteServiceBaseselect(List<RemoteServiceBase>list){RemoteServiceBaseservice=null;try{lock.tryLock(10,TimeUnit.MILLISECONDS);//若计数大于服务提供者个数,将计数器归0if(index>=list.size()){index=0;}service=list.get(index);index++;}catch(InterruptedExceptione){e.printStackTrace();}finally{lock.unlock();}//兜底,保证程序健壮性,若未取到服务,则直接取第一个if(service==null){service=list.get(0);}returnservice;}}

 

加权轮询的话,需要给服务地址添加权重:

/***加权轮询*/publicclassWeightingPollingStrategyimplementsClusterStrategy{//计数器privateintindex=0;//计数器锁privateLocklock=newReentrantLock();@OverridepublicRemoteServiceBaseselect(List<RemoteServiceBase>list){RemoteServiceBaseservice=null;try{lock.tryLock(10,TimeUnit.MILLISECONDS);//存放加权后的服务提供者列表List<RemoteServiceBase>weightingList=newArrayList<RemoteServiceBase>();for(RemoteServiceBaseremoteServiceBase:list){//扩大10倍intweight=(int)(remoteServiceBase.getWeight()*10);for(inti=0;i<weight;i++){weightingList.add(remoteServiceBase);}}//若计数大于服务提供者个数,将计数器归0if(index>=weightingList.size()){index=0;}service=weightingList.get(index);index++;returnservice;}catch(InterruptedExceptione){e.printStackTrace();}finally{lock.unlock();}//兜底,保证程序健壮性,若未取到服务,则直接取第一个returnlist.get(0);}}

 

服务调用时延

 

消费者缓存所有服务提供者的调用时延,周期性的计算服务调用平均时延。

 

然后计算每个服务提供者服务调用时延与平均时延的差值,根据差值大小动态调整权重,保证服务时延大的服务提供者接收更少的消息,防止消息堆积。

 

该策略的特点:保证处理能力强的服务接受更多的消息,通过动态的权重分配消除服务调用时延的震荡范围,使所有服务的调用时延接近平均值,实现负载均衡。

 

一致性哈希

 

相同参数的请求总是发送到统一服务提供者,当某一台服务提供者宕机时,原本发往根提供者的请求,基于虚拟节点,平摊到其他提供者,不会引起剧烈变动,平台提供默认的虚拟节点数,可以通过配置文件修改虚拟节点个数。

 

一致性 Hash 环工作原理如下图所示:

这可能是讲分布式系统最到位的一篇文章

 

一致性哈希

 

路由规则

 

负载均衡只能保证服务提供者压力的平衡,但是在一些业务场景中需要设置一些过滤规则,比较常用的是基本表达式的条件路由。

 

通过 IP 条件表达式配置黑白名单访问控制:consumerIP != 192.168.1.1。

 

只暴露部分服务提供者,防止这个集群服务都被冲垮,导致其他服务也不可用。

例如providerIP=192.168.3*。读写分离:method=find*,list*,get*,query*=>providerIP=192.168.1.。前后台分离:App=web=>providerIP=192.168.1.,app=java=>providerIP=192.168.2.。灰度升级:将WEB前台应用理由到新的服务版本上:app=web=>provicerIP=192.168.1.*。

 

序列化与反序列化

 

把对象转换为字节序列的过程称为序列化,把字节序列恢复为对象的过程称为反序列化。

 

运程调用的时候,我们需要先将 Java 对象进行序列化,然后通过网络,IO 进行传输,当到达目的地之后,再进行反序列化获取到我们想要的结果对象。

 

分布式系统中,传输的对象会很多,这就要求序列化速度快,产生字节序列小的序列化技术。

 

序列化技术:Serializable,XML,Jackson,MessagePack,FastJson,Protocol Buffer,Thrift,Gson,Avro,Hessian 等。

 

Serializable 是 Java 自带的序列化技术,无法跨平台,序列化和反序列化的速度相对较慢。

 

XML 技术多平台支持好,常用于与银行交互的报文,但是其字节序列产生较大,不太适合用作分布式通讯框架。

 

FastJson 是 Java 语言编写的高性能的 JSON 处理器,由阿里巴巴公司开发,字节序列为 json 串,可读性好,序列化也速度非常的快。

 

Protocol Buffer 序列化速度非常快,字节序列较小,但是可读性较差。

 

一般分布式服务框架会内置多种序列化协议可供选择,如 Dubbo 支持的 7 种协议用到的序列化技术就不完全相同。

 

服务调用

 

本地环境下,使用某个接口很简单,直接调用就行。分布式环境下就不是那么简单了,消费者方只会存在接口的定义,没有具体的实现。

 

想要像本地环境下直接调用远程接口那就得耗费一些功夫了,需要用到远程代理。

 

下面是我盗的图:

这可能是讲分布式系统最到位的一篇文章

 

远程代理

 

通信时序如下:

这可能是讲分布式系统最到位的一篇文章

 

通信时序

 

消费者端没有具体的实现,需要调用接口时动态的去创建一个代理类。与 Spirng 集成的情况,那直接在 Bean 构建的时候注入代理类。

 

下面是构建代理类:

importjava.lang.reflect.Proxy;publicclassJdkProxy{publicstaticObjectgetInstance(Class<?>cls){JdkMethodProxyinvocationHandler=newJdkMethodProxy();ObjectnewProxyInstance=Proxy.newProxyInstance(cls.getClassLoader(),newClass[]{cls},invocationHandler);return(Object)newProxyInstance;}}

 

importjava.lang.reflect.InvocationHandler;importjava.lang.reflect.Method;publicclassJdkMethodProxyimplementsInvocationHandler{@OverridepublicObjectinvoke(Objectproxy,Methodmethod,Object[]parameters)throwsThrowable{//如果传进来是一个已实现的具体类if(Object.class.equals(method.getDeclaringClass())){try{returnmethod.invoke(this,parameters);}catch(Throwablet){t.printStackTrace();}//如果传进来的是一个接口}else{//实现接口的核心方法//returnRemoteInvoking.invoking(serviceName,serializationType,//timeOut,loadBalanceStrategy,method,parameters);}returnnull;}}

 

代理会做很多事情,对请求服务的名称及参数信息的序列化、通过路由选择最为合适服务提供者、建立通讯连接发送请求信息(或者直接发起 Http 请求)、最后返回获取到的结果。

 

当然这里面需要考虑很多问题,如调用超时,请求异常,通讯连接的缓存,同步服务调用还是异步服务调用等等。

 

同步服务调用:客户端发起远程服务调用请求,用户线程完成消息序列化之后,将消息投递到通信框架,然后同步阻塞,等待通信线程发送请求并接收到应答之后,唤醒同步等待的用户线程,用户线程获取到应答之后返回。

 

异步服务调用:基于 Java 的 Future 机制,客户端发起远程服务调用请求,该请求会被标上 RequestId,同时建立一个与 RequestId 对应的 Future,客户端通过 Future 的 Get 方法获取结果时会被阻塞。

 

服务端收到请求应达会回传 RequestId,通过 RequestId 去解除对应 Future 的阻塞,同时 Set 对应结果,最后客户端获取到结果。

 

构建 Future,以 RequestId 为 Key,Put 到线程安全的 Map 中。Get 结果时需要写入 Time Out 超时时间,防止由于结果的未返回而导致的长时间的阻塞。

SyncFuture<RpcResponse>syncFuture=newSyncFuture<RpcResponse>();SyncFutureCatch.syncFutureMap.put(rpcRequest.getRequestId(),syncFuture);try{RpcResponserpcResponse=syncFuture.get(timeOut,TimeUnit.MILLISECONDS);returnrpcResponse.getResult();}catch(Exceptione){throwe;}finally{SyncFutureCatch.syncFutureMap.remove(rpcRequest.getRequestId());}

 

结果返回时通过回传的 RequestId 获取对应 Future 写入 Response,Future 线程解除阻塞:

log.debug("TcpClientreceivehead:"+headAnalysis+"TcpClientreceivedata:"+rpcResponse);SyncFuture<RpcResponse>syncFuture=SyncFutureCatch.syncFutureMap.get(rpcResponse.getRequestId());if(syncFuture!=null){syncFuture.setResponse(rpcResponse);}

 

importjava.util.concurrent.CountDownLatch;importjava.util.concurrent.Future;importjava.util.concurrent.TimeUnit;publicclassSyncFuture<T>implementsFuture<T>{//因为请求和响应是一一对应的,因此初始化CountDownLatch值为1。privateCountDownLatchlatch=newCountDownLatch(1);//需要响应线程设置的响应结果privateTresponse;//Futrue的请求时间,用于计算Future是否超时privatelongbeginTime=System.currentTimeMillis();publicSyncFuture(){}@Overridepublicbooleancancel(booleanmayInterruptIfRunning){returnfalse;}@OverridepublicbooleanisCancelled(){returnfalse;}@OverridepublicbooleanisDone(){if(response!=null){returntrue;}returnfalse;}//获取响应结果,直到有结果才返回。@OverridepublicTget()throwsInterruptedException{latch.await();returnthis.response;}//获取响应结果,直到有结果或者超过指定时间就返回。@OverridepublicTget(longtimeOut,TimeUnitunit)throwsInterruptedException{if(latch.await(timeOut,unit)){returnthis.response;}returnnull;}//用于设置响应结果,并且做countDown操作,通知请求线程publicvoidsetResponse(Tresponse){this.response=response;latch.countDown();}publiclonggetBeginTime(){returnbeginTime;}}

 

SyncFuture<RpcResponse>syncFuture=newSyncFuture<RpcResponse>();SyncFutureCatch.syncFutureMap.put(rpcRequest.getRequestId(),syncFuture);RpcResponserpcResponse=syncFuture.get(timeOut,TimeUnit.MILLISECONDS);SyncFutureCatch.syncFutureMap.remove(rpcRequest.getRequestId());

 

除了同步服务调用,异步服务调用,还有并行服务调用,泛化调用等调用形式。

 

高可用

 

简单的介绍了下分布式服务框架,下面来说下分布式系统的高可用。一个系统设计开发出来,三天两晚就出个大问题,导致无法使用,那这个系统也不是什么好系统。

 

业界流传一句话:"我们系统支持 X 个 9 的可靠性"。这个 X 是代表一个数字,X 个 9 表示在系统 1 年时间的使用过程中,系统可以正常使用时间与总时间(1 年)之比。

 

3 个 9:(1-99.9%)*365*24=8.76 小时,表示该系统在连续运行 1 年时间里最多可能的业务中断时间是 8.76 小时,4 个 9 即 52.6 分钟,5 个 9 即 5.26 分钟。要做到如此高的可靠性,是非常大的挑战。

 

一个大型分布式项目可能是由几十上百个项目构成,涉及到的服务成千上万,主链上的一个流程就需要流转多个团队维护的项目。

 

拿 4 个 9 的可靠性来说,平摊到每个团队的时间可能不到 10 分钟。这 10 分钟内需要顶住压力,以最快的时间找到并解决问题,恢复系统的可用。

 

下面说说为了提高系统的可靠性都有哪些方案:

 

服务检测:某台服务器与注册中心的连接中断,其提供的服务也无响应时,系统应该能主动去重启该服务,使其能正常对外提供。

 

故障隔离:集群环境下,某台服务器能对外提供服务,但是因为其他原因,请求结果始终异常。

 

这时就需要主动将该节点从集群环境中剔除,避免继续对后面的请求造成影响,非高峰时期再尝试修复该问题。至于机房故障的情况,只能去屏蔽整个机房了。

 

目前饿了么做的是异地多活,即便单边机房挂了,流量也可以全量切换至另外一边机房,保证系统的可用。

 

监控:包含业务监控、服务异常监控、DB 中间件性能的监控等,系统出现异常的时候能及时的通知到开发人员。等到线下报上来的时候,可能影响已经很大了。

 

压测:产线主链路的压测是必不可少的,单靠集成测试,有些高并发的场景是无法覆盖到的,压测能暴露平常情况无法出现的问题,也能直观的提现系统的吞吐能力。当业务激增时,可以考虑直接做系统扩容。

 

SOP 方案与演练:产线上随时都可能会发生问题,抱着出现问题时再想办法解决的态度是肯定不行的,时间根本来不及。

 

提前做好对应问题的 SOP 方案,能节省大量时间,尽快的恢复系统的正常。当然平常的演练也是不可少的,一旦产线故障可以做到从容不迫的去应对和处理。

 

除了上述方案外,还可以考虑服务策略的使用:

 

降级策略:业务高峰期,为了保证核心服务,需要停掉一些不太重要的业务。

 

如双十一期间不允许发起退款、只允许查看 3 个月之内的历史订单等业务的降级,调用服务接口时,直接返回的空结果或异常等服务的降级,都属于分布式系统的降级策略。

 

服务降级是可逆操作,当系统压力恢复到一定值不需要降级服务时,需要去除降级,将服务状态恢复正常。

 

服务降级主要包括屏蔽降级和容错降级:

  • 屏蔽降级:分布式服务框架直接屏蔽对远程接口的请求,不发起对远程服务的调用,直接返回空结果、抛出指定异常、执行本地模拟接口实现等方式。
  • 容错降级:非核心服务不可调用时,可以对故障服务做业务放通,保证主流程不受影响。如请求超时、消息解码异常、系统拥塞保护异常, 服务提供方系统异常等情况。

 

笔者之前就碰到过因双方没有做容错降级导致的系统故障的情况。午高峰时期,对方调用我们的一个非核心查询接口,我们系统因为 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,通过它了解了分布式服务框架内部的一些活动。

 

本文中所有代码都能在该项目中找到,有兴趣读者可以看看:

https://github.com/wangshiyu/blackRpc


Tags:分布式系统   点击:()  评论:()
声明:本站部分内容来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除,谢谢。
▌相关评论
发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表
▌相关推荐
分布式系统如何寻址?通过 RPC 框架,能够解决服务之间的跨网络通信问题,是微服务改造的基础。服务拆分之后,需要维护更多细粒度的服务,这样就涉及到 RPC 客户端服到服务端的 部署...【详细内容】
2020-11-04   分布式系统  点击:(3)  评论:(0)  加入收藏
上一篇《CAP》写完之后,我又反复回看了多次,发现最后的一部分表达CAP、ACID、BASE、“BACP(自造)”关系时有一些问题,并且不是很严谨,但是无奈已经发送过的内容,无法支持修改,并且有挺多小伙伴都在私聊我确认细节,这里我来重...【详细内容】
2020-09-16   分布式系统  点击:(8)  评论:(0)  加入收藏
介绍OAuth(开放授权)是一个开放标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方应用或分享他们数据的所有内容。OAuth...【详细内容】
2020-08-18   分布式系统  点击:(6)  评论:(0)  加入收藏
3 分布式系统抽象讨论编程语言时,我们使用通用术语并用函数、运算符、类、变量和指针来定义我们的程序。通用的词汇可以帮助我们避免每次都为了描述某些东西而发明新词。我们...【详细内容】
2020-08-02   分布式系统  点击:(4)  评论:(0)  加入收藏
分布式系统架构之构建你的任务调度中心分布式系统中,我们经常会遇到定时执行任务,而这些定时任务中,多数情况都是需要执行一些http请求。比如: 轮训支付结果(虽然第三方支付中心...【详细内容】
2020-07-17   分布式系统  点击:(4)  评论:(0)  加入收藏
系统唯一ID是我们在设计一个系统的时候常常会遇见的问题,也常常为这个问题而纠结。生成ID的方法有很多,适应不同的场景、需求以及性能要求。所以有些比较复杂的系统会有多个ID...【详细内容】
2020-06-18   分布式系统  点击:(1)  评论:(0)  加入收藏
大型主机缺点:非常贵,一般的小企业用不起。比较复杂,培养人才的成本比较高。单点一旦故障,整个系统停转,损失非常大。个人PC电脑的性能越来越高,成本也越来越低。 Web时代c/...【详细内容】
2020-06-12   分布式系统  点击:(6)  评论:(0)  加入收藏
高可用是指系统无中断的执行功能的能力,代表了系统的可用程度,是进行系统设计时必须要遵守的准则之一。 而高可用的实现方案,无外乎就是冗余,就存储的高可用而言,问题不在于如何...【详细内容】
2020-05-25   分布式系统  点击:(8)  评论:(0)  加入收藏
在计算机系统的领域,一致性可以说是一个高频词,可能出现的场景很多。从分布式系统到数据库的事务,都有它的身影。之前我们在介绍数据库事务的时候,谈到过事务的一致性。在数据库...【详细内容】
2020-03-30   分布式系统  点击:(7)  评论:(0)  加入收藏
分布式系统中日志追踪需要考虑的几个点? 需要一个全服务唯一的id,即traceId,如何保证? traceId如何在服务间传递? traceId如何在服务内部传递? traceId如何在多线程中传递?我们一一...【详细内容】
2020-03-16   分布式系统  点击:(6)  评论:(0)  加入收藏
什么是分布式系统分布式系统是由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成的系统。分布式系统的出现是为了用廉价的、普通的机器完成单个计算机...【详细内容】
2020-03-15   分布式系统  点击:(11)  评论:(0)  加入收藏
01 大数据时代的新挑战:实时流计算社会需求和科技进步是螺旋式相互促进和提升的。“大数据”一词最早由Roger Mougalas在2005年提出,所以我们姑且认为2005年是大数据时代的元...【详细内容】
2020-03-14   分布式系统  点击:(17)  评论:(0)  加入收藏
定义软件架构师是软件行业中一种新兴职业,工作职责是在一个软件项目开发过程中,将客户的需求转换为规范的开发计划及文本,并制定这个项目的总体架构,指导整个开发团队完成这个计...【详细内容】
2020-03-14   分布式系统  点击:(27)  评论:(0)  加入收藏
一个网站就是一个应用,当系统压力较大时,只能横向扩展,增加多个服务器或者多个容器去做负载均衡,避免单点故障而影响到整个系统。 集中式最明显的优点就是开发,测试,运维会比较...【详细内容】
2020-01-26   分布式系统  点击:(14)  评论:(0)  加入收藏
一、缓存概述缓存是分布式系统中的重要组件,主要解决高并发,大数据场景下,热点数据访问的性能问题。提供高性能的数据快速访问。1、缓存的原理 将数据写入/读取速度更快的存储(...【详细内容】
2020-01-07   分布式系统  点击:(34)  评论:(0)  加入收藏
单机的 redis,能够承载的 QPS 大概就在上万到几万不等。对于缓存来说,一般都是用来支撑读高并发的。因此架构做成主从(master-slave)架构,一主多从,主负责写,并且将数据复制到其...【详细内容】
2019-12-26   分布式系统  点击:(29)  评论:(0)  加入收藏
事务机制ACID和CAP理论是数据管理和分布式系统中两个重要的概念,很不巧,这两个概念中都有相同的“C”代表 "Consistency" 一致性,但是实际上是完全不同的意义,下面是比较两个概...【详细内容】
2019-11-27   分布式系统  点击:(29)  评论:(0)  加入收藏
1无论是我们在学校刚开始学编程,还是在刚参加工作开始处理实际问题,写出来的程序都是很简单的。因为面对的问题很简单。以处理数据为例,可能只是把一个几十 K 的文件解析下,然后...【详细内容】
2019-11-26   分布式系统  点击:(19)  评论:(0)  加入收藏
一个优秀的架构师,抽象思维能力是必不可少的,架构师要善于“店丁解牛”,将实物概念化并归类,比如一个大型网站,你能够迅速根据业务功能的不同,将业务垂直化;而扎实的技术功底又是架...【详细内容】
2019-11-15   分布式系统  点击:(29)  评论:(0)  加入收藏
作者:季雨林 来源:https://www.opengps.cn/Blog/View.aspx?id=426关于怎么实现承载更多用户量的系统,一直是我重点关注的一个技术方向。改造架构提高承载力,通常来讲分为两个大...【详细内容】
2019-10-30   分布式系统  点击:(139)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条