分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统。
单一应用架构
一个war包将所有功能都部署在一起搞定,减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是关键。

优点:适用与小型网站、简单易用
缺点:性能扩展难、协同开发问题、不利于升级维护
垂直应用架构
当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的Web框架(MVC)是关键。

优点:团队各司其职更易管理、性能扩展也更方便、更有针对性
缺点:公用模块无法重复利用,开发性的浪费
分布式服务架构
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式服务框架(RPC)[Remote Procedure Call]是关键。

优点:增大系统容量、模块化高、开发及团队协作效率高
缺点:系统设计、管理和运维难度增加
流动计算架构
当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)[Service Oriented Architecture]是关键

优点:增大系统容量、模块化高、动态协调资源、开发及团队协作效率高
缺点:系统设计、管理和运维难度增加
RPC定义
RPC【Remote Procedure Call】是指远程过程调用,是一种进程间通信方式,他是一种技术的思想,而不是规范。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节。即程序员无论是调用本地的还是远程的函数,本质上编写的调用代码基本相同。
RPC基本原理

RPC最核心两个模块:通讯、序列化
dubbo是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。

服务提供者(Provider):暴露服务的服务提供方,服务提供者在启动时,向注册中心注册自己提供的服务。
服务消费者(Consumer): 调用远程服务的服务消费方,服务消费者在启动时,向注册中心订阅自己所需的服务,服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
注册中心(Registry):注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者
监控中心(Monitor):服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心
调用关系说明
连通性说明
前提条件:配置zookeeper环境、incubator-dubbo-ops切换到master分支【dubbo-admin和dubbo-monitor】
搭建Dubbo Admin、搭建Dubbo Monitor方法详见GitHub参考文档
创建maven项目公共项目抽取公共部分【bean、service接口等等】
创建provider和consumer
<!-- 连接zookeeper使用的curator -->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.13.0</version>
<exclusions>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 日志采用slfj+logback -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.6</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.2.3</version>
</dependency>
<!-- 提供方应用信息,用于计算依赖关系 -->
<dubbo:application name="user-service-provider" />
<!-- 使用zookeeper广播注册中心暴露服务地址 -->
<dubbo:registry protocol="zookeeper" address="192.168.1.155:2181,192.168.1.155:2182,192.168.1.155:2183"/>
<!-- 用dubbo协议在20800端口暴露服务 -->
<dubbo:protocol name="dubbo" port="20800"/>
<!-- 声明需要暴露的服务接口 -->
<dubbo:service interface="com.kun.service.UserService" ref="userService"/>
<!-- 和本地bean一样实现服务 -->
<bean id="userService" class="com.kun.service.impl.UserServiceImpl"/>
<!-- 消费方应用名,用于计算依赖关系,不是匹配条件,不要与提供方一样 -->
<dubbo:application name="user-service-consumer" />
<!-- 使用zookeeper注册中心暴露发现服务地址 -->
<dubbo:registry protocol="zookeeper" address="192.168.1.155:2181,192.168.1.155:2182,192.168.1.155:2183" />
<!-- 生成远程服务代理,可以像本地bean一样使用userService -->
<dubbo:reference id="userService" interface="com.kun.service.UserService" ></dubbo:reference>
启动Provider在Dubbo Admin可看相关信息,启动Consumer进行消费在Dubbo Admin也可见相关信息
Dubbo配置文件加载顺序
覆盖关系的优先级:System Properties【启动时的环境变量】> Externlized Configuration【使用控制台调节】 > Spring/Api【硬编码】 > Local File【本地配置文件】
Dubbo配置的覆盖关系
Dubbo启动检查
dubbo:reference&dubbo:consumer设定了启动时是否检查,默认取dubbo:consumer的true
Dubbo超时设置
timeout为dubbo:method、dubbo:reference、dubbo:service、dubbo:consumer、dubbo:provider 属性 默认设置为1000。timeout为服务超时时间
Dubbo重试
retries为dubbo:method、dubbo:reference、dubbo:service、dubbo:consumer、dubbo:provider 属性默认设置为2。retries为当服务未返回结果【由于网络、超时等原因】重试次数,不包含第一次请求
幂等操作【查询、删除、修改】应当设置重试次数提高服务的可用性、非幂等操作【新增】为保证数据的正确性不应当设置重试次数
Dubbo多版本
version为dubbo:method、dubbo:reference、dubbo:service、dubbo:consumer、dubbo:provider 、dubbo:application属性。version用于实现灰度发布、测试版本区分等
Dubbo本地存根
stub为dubbo:reference、dubbo:service属性。一般为dubbo:reference使用,为调用服务之前处理相关业务逻辑,一般情况下Stub类存放在抽取的接口工程中
<dubbo:reference id="userService" interface="com.kun.service.UserService" stub="com.kun.service.impl.UserServiceStub"></dubbo:reference>
public class UserServiceStub implements UserService {
private final UserService userService;
//必须有Service作为参数的构造函数
public UserServiceStub(UserService userService) {
this.userService = userService;
}
@Override
public List<UserAddress> getUserAddressList(String userId) {
System.out.println("调用本地存根进行参数验证。。。");
if(userId != null && !userId.trim().isEmpty()) {
return userService.getUserAddressList(userId);
}
return null;
}
}
线程模型
如果事件处理的逻辑能迅速完成,并且不会发起新的 IO 请求,比如只是在内存中记个标识,则直接在 IO 线程上处理更快,因为减少了线程池调度。
但如果事件处理逻辑较慢,或者需要发起新的 IO 请求,比如需要查询数据库,则必须派发到线程池,否则 IO 线程阻塞,将导致不能接收其它请求。
如果用 IO 线程处理事件,又在事件处理过程中发起新的 IO 请求,比如在连接事件中发起登录请求,会报“可能引发死锁”异常,但不会真死锁。
因此,需要通过不同的派发策略和不同的线程池配置的组合来应对不同的场景,缺省设置如下
<dubbo:protocol name="dubbo" dispatcher="all" threadpool="fixed" threads="200" />
Dispatcher
all 所有消息都派发到线程池,包括请求,响应,连接事件,断开事件,心跳等。direct 所有消息都不派发到线程池,全部在 IO 线程上直接执行。message 只有请求响应消息派发到线程池,其它连接断开事件,心跳等消息,直接在 IO 线程上执行。execution 只请求消息派发到线程池,不含响应,响应和其它连接断开事件,心跳等消息,直接在 IO 线程上执行。connection 在 IO 线程上,将连接断开事件放入队列,有序逐个执行,其它消息派发到线程池。ThreadPool
fixed 固定大小线程池,启动时建立线程,不关闭,一直持有。(缺省)cached 缓存线程池,空闲一分钟自动删除,需要时重建。limited 可伸缩线程池,但池中的线程数只会增长不会收缩。只增长不收缩的目的是为了避免收缩时突然来了大流量引起的性能问题。eager 优先创建Worker线程池。在任务数量大于corePoolSize但是小于maximumPoolSize时,优先创建Worker来处理任务。当任务数量大于maximumPoolSize时,将任务放入阻塞队列中。阻塞队列充满时抛出RejectedExecutionException。(相比于cached:cached在任务数量超过maximumPoolSize时直接抛出异常而不是将任务放入阻塞队列)| 属性 | 描述 |
|---|---|
| threadpool | 线程池类型,默认fixed |
| threads | 服务线程池大小(固定大小),默认200 |
| iothreads | IO线程池,接收网络读写中断,以及序列化和反序列化,不处理业务,此线程池和CPU相关,不建议配置 |
| accepts | 服务提供者最大可接受连接数,默认0 |
| dispatcher | 协议的消息派发方式,用于指定线程模型,默认all |
| queues | 线程池队列大小,当线程池满时,排队等待执行的队列大小,建议不要设置,当线程程池时应立即失败,重试其它服务提供机器,而不是排队,除非有特殊需求 |
如果服务器抛出
java.lang.OutOfMemoryError: unable to create new native thread unable to create new native thread异常可能是启动用户线程数限制导致的,需修改/etc/security/limits.d目录下的文件,最大限制数使用ulimit -u查看用户可创建线程的最大数,必须要小于这个数值否则线程占用完无法登陆主机导致死机现象
直连提供者
在开发及测试环境,绕过注册中心直连提供者,方便开发调试,线上勿用
<dubbo:reference id="xxxService" interface="com.kun.xxx.XxxService" url="dubbo://localhost:2090" />
只订阅
为方便开发测试,服务可能需要连接注册中心以依赖其他服务,而自己并不想注册在注册中心,可使用dubbo:registry中的registry属性控制是否只订阅

只注册
当有多个注册中心,需要只注册到一部分注册中心【依赖这部分注册中心的服务】,另一部分注册中心只提供服务【不依赖提供的服务】,使用dubbo:registry中的subscribe属性控制是否只注册
使用com.alibaba.dubbo.container.Main启动可实现服务启动和优雅停机
<!-- 打包jar文件时,配置manifest文件,加入lib包的jar依赖 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<classesDirectory>target/classes/</classesDirectory>
<archive>
<manifest>
<!-- 启动类 -->
<mainClass>com.alibaba.dubbo.container.Main</mainClass>
<!-- 打包时MANIFEST.MF 文件不记录时间戳-->
<useUniqueVersions>false</useUniqueVersions>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
</manifest>
</archive>
</configuration>
</plugin>
可使用dubbo包下的/META-INF/assembly/bin/*.sh脚本管理程序的启动、停止、dump等
注:dubbo.spring.config默认为META-INF/spring,dubbo启动时会加载此路径下的文件为Spring配置文件
Dubbo是通过JDK的ShutdownHook完成优雅停机的,所以使用 kill -9 PID 等强制关闭指令只会强制关闭不会优雅停机,只有通过 kill PID 时,才会优雅停机
zookeeper宕机
现象:zookeeper注册中心宕机,还可以消费dubbo暴露的服务。
监控中心宕掉不影响使用,只是丢失部分采样数据
数据库宕掉后,注册中心仍能通过缓存提供服务列表查询,但不能注册新服务
注册中心对等集群,任意一台宕掉后,将自动切换到另一台
注册中心全部宕掉后,服务提供者和服务消费者仍能通过本地缓存通讯
服务提供者无状态,任意一台宕掉后,不影响使用
服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复
dubbo直连
url为dubbo:reference的属性。提供了dubbo直连方式【只需提供IP地址和端口号】,不经过注册中心


最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。

一致性 Hash,相同参数的请求总是发到同一提供者。

默认Dubbo采用Random LoadBalance负载均衡策略,可在dubbo:service、dubbo:reference、dubbo:method的loadbalance修改负载均衡策略
场景:当服务器压力剧增的情况下,根据实际业务情况及流量,对一些服务和页面有策略的不处理或换种简单的方式处理,从而释放服务器资源以保证核心交易正常运作或高效运作
处理方式:可以通过服务降级功能临时屏蔽某个出错的非关键服务,并定义降级后的返回策略
mock=force:return+null 表示消费方对该服务的方法调用都直接返回 null 值,不发起远程调用。用来屏蔽不重要服务不可用时对调用方的影响。【admin界面中点击【屏蔽】】mock=fail:return+null 表示消费方对该服务的方法调用在失败后,再返回 null 值,不抛异常。用来容忍不重要服务不稳定时对调用方的影响【admin界面中点击【容错】】在集群调用失败时,Dubbo 提供了多种容错方案,缺省为 failover 重试
Failover Cluster
失败自动切换,当出现失败,重试其它服务器 。通常用于读操作,但重试会带来更长延迟。可通过 retries="2" 来设置重试次数(不含第一次)
Failfast Cluster
快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录
Failsafe Cluster
失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作
Failback Cluster
失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作
Forking Cluster
并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks="2" 来设置最大并行数
Broadcast Cluster
广播调用所有提供者,逐个调用,任意一台报错则报错 。通常用于通知所有提供者更新缓存或日志等本地资源信息
可在dubbo:service和dubbo:reference中配置集群容错策略
配置spring-cloud-starter-netflix-hystrix
1、导入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>1.4.4.RELEASE</version>
</dependency>
2、在主配置类上添加@EnableHystrix
@SpringBootApplication
@EnableHystrix
public class ProviderApplication {
//......
}
3、配置Provider端
在Dubbo的Provider上增加@HystrixCommand配置,这样子调用就会经过Hystrix代理
@Service(version = "1.0.0")
public class HelloServiceImpl implements HelloService {
@HystrixCommand(commandProperties = {
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000") })
@Override
public String sayHello(String name) {
throw new RuntimeException("Exception to show hystrix enabled.");
}
}
4、配置Consumer端
对于Consumer端,则可以增加一层method调用,并在method上配置@HystrixCommand。当调用出错时,会走到fallbackMethod = “reliable”的调用里
@Reference(version = "1.0.0")
private HelloService helloService;
@HystrixCommand(fallbackMethod = "reliable")
public String doSayHello(String name) {
return helloService.sayHello(name);
}
public String reliable(String name) {
return "hystrix fallback value";
}

ServiceConfig, ReferenceConfig 为中心,可以直接初始化配置类,也可以通过 spring 解析配置生成配置类ServiceProxy为中心,扩展接口为 ProxyFactoryRegistryFactory, Registry, RegistryServiceInvoker 为中心,扩展接口为 Cluster, Directory, Router, LoadBalanceStatistics 为中心,扩展接口为 MonitorFactory, Monitor, MonitorServiceInvocation, Result 为中心,扩展接口为 Protocol, Invoker, ExporterRequest, Response 为中心,扩展接口为 Exchanger, ExchangeChannel, ExchangeClient, ExchangeServerMessage 为中心,扩展接口为 Channel, Transporter, Client, Server, CodecSerialization, ObjectInput, ObjectOutput, ThreadPoolDubbo扩展了Java 原生的 SPI 机制会加载META-INF/dubbo下的配置文件,加载到Spring容器中
DubboNamespaceHandler集成了Spring下的NamespaceHandlerSupportj,用于解析dubbo名称空间标签
public class DubboNamespaceHandler extends NamespaceHandlerSupport {
//用于初始化各种解析器
@Override
public void init() {
registerBeanDefinitionParser("application", new DubboBeanDefinitionParser(ApplicationConfig.class, true));
registerBeanDefinitionParser("module", new DubboBeanDefinitionParser(ModuleConfig.class, true));
registerBeanDefinitionParser("registry", new DubboBeanDefinitionParser(RegistryConfig.class, true));
registerBeanDefinitionParser("config-center", new DubboBeanDefinitionParser(ConfigCenterBean.class, true));
registerBeanDefinitionParser("metadata-report", new DubboBeanDefinitionParser(MetadataReportConfig.class, true));
registerBeanDefinitionParser("monitor", new DubboBeanDefinitionParser(MonitorConfig.class, true));
registerBeanDefinitionParser("provider", new DubboBeanDefinitionParser(ProviderConfig.class, true));
registerBeanDefinitionParser("consumer", new DubboBeanDefinitionParser(ConsumerConfig.class, true));
registerBeanDefinitionParser("protocol", new DubboBeanDefinitionParser(ProtocolConfig.class, true));
registerBeanDefinitionParser("service", new DubboBeanDefinitionParser(ServiceBean.class, true));
registerBeanDefinitionParser("reference", new DubboBeanDefinitionParser(ReferenceBean.class, false));
registerBeanDefinitionParser("annotation", new AnnotationBeanDefinitionParser());
}
}
DubboBeanDefinitionParser实现了Spring下的BeanDefinitionParser,提供了标签解析功能

服务导出的入口方法是ServiceBean的onApplicationEvent,在Spring容器上下文刷新后回调,执行暴露服务
public void onApplicationEvent(ContextRefreshedEvent event) {
// 是否有延迟导出 && 是否已导出 && 是不是已被取消导出
if (isDelay() && !isExported() && !isUnexported()) {
// 导出服务
export();
}
}
暴露过程中发现注册中心时zookeeper会将服务信息注册在zookeeper上

暴露完成后会启动Netty服务,持续提供服务

服务引用的入口方法为ReferenceBean的实现FactoryBean中的getObject方法
public Object getObject() throws Exception {
return get();
}
public synchronized T get() {
if (destroyed) {
throw new IllegalStateException("Already destroyed!");
}
// 检测 ref 是否为空,为空则通过 init 方法创建
if (ref == null) {
// init 方法主要用于处理配置,以及调用 createProxy 生成代理类
init();
}
return ref;
}
init()方法中使用createProxy()方法实现创建代理对象

突破口是InvokerInvocationHandler实现InvocationHandler接口的invoke方法,得到调用结果,最后将结果转型并返回给调用方
public class InvokerInvocationHandler implements InvocationHandler {
private final Invoker<?> invoker;
public InvokerInvocationHandler(Invoker<?> handler) {
this.invoker = handler;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
Class<?>[] parameterTypes = method.getParameterTypes();
// 拦截定义在 Object 类中的方法(未被子类重写),比如 wait/notify
if (method.getDeclaringClass() == Object.class) {
return method.invoke(invoker, args);
}
// 如果 toString、hashCode 和 equals 等方法被子类重写了,这里也直接调用
if ("toString".equals(methodName) && parameterTypes.length == 0) {
return invoker.toString();
}
if ("hashCode".equals(methodName) && parameterTypes.length == 0) {
return invoker.hashCode();
}
if ("equals".equals(methodName) && parameterTypes.length == 1) {
return invoker.equals(args[0]);
}
// 根据不同的invoker使用不同的集群容错策略,调用提供者的方法
return invoker.invoke(new RpcInvocation(method, args)).recreate();
}
}