Tallate

该吃吃该喝喝 啥事别往心里搁

为什么使用 Dubbo

选型时一般需要考虑:

  1. 业务特点及可预见的后续的发展。
  2. 可用性要求。
  3. 团队的成熟度。一个成熟的团队可以很好地 Hold 住复杂的开源框架,甚至做定制化开发。

在选择使用 Dubbo 之后,又需要考虑很多细节,比如:

  1. Dubbo 底层走什么协议?如何对对象进行序列化,用了哪些序列化方式?如何处理异步转同步?
  2. 高并发高可用性。Dubbo 依赖了 ZooKeeper,但是万一 ZooKeeper 宕机了怎么办?
    如果 ZooKeeper 假死,客户端对服务端的调用是否会全部下线?如果是该如何避免?
    如何监控 Dubbo 的调用,并做到优雅的客户端无感发布?

最佳实践

  1. 模块化
    推荐将服务接口、实体、异常等都放到 API 包内,它们都是 API 的一部分。
  2. 粗粒度
    暴露的 Dubbo 接口的粒度应尽可能得粗,代表一个完整的功能,而不是其中的某一步,否则就不得不面对分布式事务问题了,而 Dubbo 当前并没有提供分布式事务支持。
  3. 版本
    某露服务接口的配置最好增加版本,当有不兼容的升级(比如接口定义要加个参数)时,版本可以方便地实现平滑发布,而又不用引入多余的代码。
    版本只需要两位即可,比如"1.0",因为升级并不是频繁的操作,因为不兼容的升级不会那么频繁。
    升级时,先将一半的 provider 升级到新版本,然后将所有 consumer 升级,最后将其余的 provider 升级。
  4. 兼容性
    向后兼容:接口加方法、对象加字段;
    不兼容:删除方法、删除字段、枚举类型加字段。
    不兼容的情况下,可以通过升级版本来实现平滑发布。
  5. 枚举类型
    枚举是类型安全的,但是作为 Dubbo 接口的参数 / 返回值却不合适,因为 provider 会将枚举转换为字符串传输,接收方会尝试寻找该字符串所属的枚举 field,找不到就会直接报错。
  6. 序列化
    传值没必要使用接口抽象,因为序列化需要接口实现类的元信息(包括 getter、setter),无法隐藏实现。
    参数和返回值必须 byValue 而不是 byReference,因为 Dubbo 不支持远程对象,provider 引用的对象 consumer 就找不到了。
  7. 异常
    最好直接抛异常而不是返回异常码,因为异常可以携带更多信息、语法上也更加友好。
    provider 不要将 DAO 层的异常抛给 consumer 端,consumer 端不应该关注 provider 对服务是如何实现的。

开始使用 Dubbo

ZooKeeper

ZooKeeper 在 Dubbo 中可以作为注册中心使用。
下载 ZooKeeper,修改配置,配置文件位于{ZOOKEEPER_HOME}/conf/zoo.cfg:

1
2
3
4
5
dataDir = /tmp/zk/data
clientPort = 2181
tickTime = 2000
initLimit = 5
syncLimit = 2
  • dataDir:数据保存的目录
  • clientPort:监听的端口
  • tickTime:心跳检查间隔
  • initLimit:Follower 启动从 Leader 同步数据时能忍受多少个心跳的时间间隔
  • syncLimit:Leader 同步到 Follower 后,如果超过 syncLimit 个 tickTime 的时间过去,还没有收到 Follower 的响应,那么就认为该 Follower 已下线。

后台启动:

1
./bin/zkServer.sh start-foreground

SDK

SDK 是一个被 provider 和 consumer 同时依赖的 jar 包,它的作用包括:

  • 提供实体类的定义;
    1
    2
    3
    public class Person {
    ...
    }
  • 提供接口的定义;
    1
    2
    3
    public interface UserServiceBo {
    String sayHello(String name);
    }

在设计 SDK 时包含一些注意要点,比如:

  • 不要使用枚举,用字符串常量来替代,因为 Dubbo 反序列化时如果碰到不存在的枚举就会抛出异常,这个问题编译期无法发现,可能造成线上故障;
  • 升级时不要随意修改接口定义,provider 和 consumer 接口定义不同会导致运行时故障,最佳实践是提升dubbo:referencedubbo:service的版本号,或者直接增加一个接口。

Provider

  1. 声明依赖
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>dubbo</artifactId>
    <version>2.6.6</version>
    </dependency>
    <dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.0.35.Final</version>
    </dependency>
    <dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-framework</artifactId>
    <version>4.2.0</version>
    </dependency>
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.47</version>
    </dependency>
  2. Dubbo 配置文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://code.alibabatech.com/schema/dubbo
    http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

    <!-- 提供方应用信息,用于计算依赖关系 -->
    <dubbo:application name="dubboProvider"/>

    <!-- 使用zookeeper注册中心暴露服务地址 -->
    <dubbo:registry address="zookeeper://127.0.0.1:2181"/>

    <!-- 用dubbo协议在20880端口暴露服务 -->
    <dubbo:protocol name="dubbo" port="20880"/>
    <!-- 启用monitor模块 -->
    <dubbo:monitor protocol="registry"/>

    <bean id="userService" class="com.tallate.provider.UserServiceImpl"/>

    <!-- 声明需要暴露的服务接口 -->
    <dubbo:service interface="com.tallate.UserServiceBo" ref="userService"
    group="dubbo" version="1.0.0" timeout="3000"/>

    </beans>
  3. 接口的实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class UserServiceImpl implements UserServiceBo {

    @Override
    public String sayHello(String name) {
    //让当前当前线程休眠2s
    try {
    Thread.sleep(2000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }

    return name;
    }
    }
  4. 启动
    原生 Spring 的启动方式:
    1
    2
    3
    4
    5
    public static void main(String[] arg) throws InterruptedException {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:provider.xml");
    //挂起当前线程,如果没有改行代码,服务提供者进程会消亡,服务消费者就发现不了提供者了
    Thread.currentThread().join();
    }

    如果需要以 SpringBoot 或 Docker 方式启动可以参考官方的示例

Consumer

  1. 声明依赖
    同 Provider
  2. Dubbo 配置文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://code.alibabatech.com/schema/dubbo
    http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

    <!-- 消费方应用名,用于计算依赖关系,不是匹配条件,不要与提供方一样 -->
    <dubbo:application name="dubboConsumer" />

    <!-- 使用multicast广播注册中心暴露发现服务地址 -->
    <dubbo:registry protocol="zookeeper" address="zookeeper://127.0.0.1:2181" />
    <!-- 启动monitor-->
    <dubbo:monitor protocol="registry" />
    <!-- 生成远程服务代理,可以和本地bean一样使用demoService -->
    <dubbo:reference id="userService" interface="com.tallate.UserServiceBo" group="dubbo" version="1.0.0" timeout="3000"/>

    </beans>

    这里出现了一些以 dubbo 作为前缀的标签,它们是由 Dubbo 的扩展 DubboNamespaceHandler 来处理的,DubboBeanDefinitionParser 在解析完后会得到对应 BeanDefinition,然后生成对象放到 BeanFactory 中。

  3. 启动
    1
    2
    3
    4
    5
    6
    7
    8
    public static void main(String[] args) {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(
    new String[]{"classpath:consumer.xml"});

    final UserServiceBo demoService = (UserServiceBo) context.getBean("userService");

    System.out.println(demoService.sayHello("Hello World"));
    }

调用 Dubbo 原生 API 启动

  1. Provider
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    // 等价于<bean id="userService" class="com.test.UserServiceImpl" />
    UserServiceBo userService = new UserServiceImpl();
    // 等价于<dubbo:application name="dubboProvider" />
    ApplicationConfig application = new ApplicationConfig();
    application.setName("dubboProvider");

    // 等价于<dubbo:registry address="zookeeper://127.0.0.1:2181" />
    RegistryConfig registry = new RegistryConfig();
    registry.setAddress("127.0.0.1:2181");
    registry.setProtocol("zookeeper");

    // 等价于<dubbo:protocol name="dubbo" port="20880" />
    ProtocolConfig protocol = new ProtocolConfig();
    protocol.setName("dubbo");
    protocol.setPort(20880);

    // 等价于<dubbo:monitor protocol="registry" />
    MonitorConfig monitorConfig = new MonitorConfig();
    monitorConfig.setProtocol("registry");

    // 等价于<dubbo:service interface="com.test.UserServiceBo" ref="userService"
    // group="dubbo" version="1.0.0" timeout="3000"/>
    // 此实例很重,封装了与注册中心的连接,请自行缓存,否则可能造成内存和连接泄漏
    ServiceConfig<UserServiceBo> service = new ServiceConfig<>();
    service.setApplication(application);
    service.setMonitor(monitorConfig);
    // 多个注册中心可以用setRegistries()
    service.setRegistry(registry);
    // 多个协议可以用setProtocols()
    service.setProtocol(protocol);
    service.setInterface(UserServiceBo.class);
    service.setRef(userService);
    service.setVersion("1.0.0");
    service.setGroup("dubbo");
    service.setTimeout(3000);
    service.export();

    // 挂起当前线程
    Thread.currentThread().join();
  2. Consumer
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    // 等价于<dubbo:application name="dubboConsumer" />
    ApplicationConfig application = new ApplicationConfig();
    application.setName("dubboConsumer");

    // 等价于<dubbo:registry protocol="zookeeper" address="zookeeper://127.0.0.1:2181" />
    RegistryConfig registry = new RegistryConfig();
    registry.setAddress("127.0.0.1:2181");
    registry.setProtocol("zookeeper");

    // 等价于 <dubbo:monitor protocol="registry" />
    MonitorConfig monitorConfig = new MonitorConfig();
    monitorConfig.setProtocol("registry");

    //等价于<dubbo:reference id="userService" interface="com.test.UserServiceBo"
    //group="dubbo" version="1.0.0" timeout="3000" />
    // 此实例很重,封装了与注册中心的连接以及与提供者的连接,最好放缓存,否则可能造成内存和连接泄漏
    ReferenceConfig<UserServiceBo> reference = new ReferenceConfig<>();
    reference.setApplication(application);
    // 多个注册中心可以用setRegistries()
    reference.setRegistry(registry);
    reference.setInterface(UserServiceBo.class);
    reference.setVersion("1.0.0");
    reference.setGroup("dubbo");
    reference.setTimeout(3000);
    reference.setInjvm(false);
    reference.setMonitor(monitorConfig);

    UserServiceBo userService = reference.get();
    System.out.println(userService.sayHello("哈哈哈"));
    Thread.currentThread().join();

泛化调用

正常情况下我们使用 Dubbo 时会将实体类和接口定义放到一个 SDK 包内,其实也可以不加入这个包、直接将要传的参数放到一个 Map 对象内,称为泛化调用,但是这种方式没有什么实践价值,在此就不赘述了。

Dubbo 架构

Dubbo 是一个分布式服务框架,是阿里巴巴 SOA 服务化治理方案的核心框架,致力于提供高性能和透明化的 RPC 远程服务调用方案,以及 SOA 服务治理方案。简而言之,Dubbo 是个远程服务调用的分布式框架(告别 Web Service 模式中的 WSdl,以服务提供者与消费者的方式在 dubbo 上注册)。

Apache Dubbo 是一款高性能、轻量级的开源 Java RPC 框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。
Dubbo 的架构基本上可以概括为 RPC+服务发现,或者可以称之为弹性 RPC 框架。

CP+三大中心

Dubbo架构

图中的 Consumer 和 Provider 是抽象概念,只是想让看图者更直观的了解哪些类分属于客户端与服务器端,不用 Client 和 Server 的原因是 Dubbo 在很多场景下都使用 Provider、Consumer、Registry、Monitor 划分逻辑拓普节点,保持统一概念。

Provider: 暴露服务的服务提供方,启动时会注册自己提供的服务到注册中心。
Consumer: 调用远程服务的服务消费方,启动时会去注册中心订阅自己需要的服务,服务注册中心异步提供 Provider 的地址列表,Consumer 根据路由规则和预设的负载均衡算法选择一个 Provider 的 IP 进行调用,调用是直连的,失败后会调用另外一个。
Registry: 服务注册与发现的注册中心。
Monitor: 统计服务的调用次调和调用时间的监控中心,Provider 和 Consumer 在内存中累计调用次数和耗时,并定时每分钟发送一次统计数据到监控中心。
Container: 服务运行容器。

大数据量传输时适合用短连接,小数据量高并发适合用长连接。从上图中可以得知,Provider 和 Consumer 均通过长连接与注册中心通信,当消费方调用服务时,会创建一个连接,然后同时会创建一个心跳发送的定时线程池,每一分钟发送一次心跳包到注册中心,通过 ping-pong 来检查连接的存活性,同时还会启动断线重连定时线程池,每两秒钟检查一次连接状态,如果断开就重连,而当注册中心断开连接后,会回调通知 Consumer 销毁连接,同理,Provider 也是通过长连接与注册中心通信。

元数据中心

2.7 之后提供的一个新组件,容易和注册中心混淆,元数据和注册中心中的注册信息之间的区别如下:

  • 元数据(Metadata)指的是服务分组、服务版本、服务名、方法列表、方法参数列表、超时时间等
  • 注册信息指服务分组、服务版本、服务名、地址列表等。

元数据中心和注册中心包含了一些公共数据,另外,元数据中心还会存储方法列表即参数列表,注册中心存储了服务地址,其他的一些区别如下所示:

  • | 元数据 | 注册信息
  • | - | -
    职责 | 描述服务,定义服务的基本属性 | 存储地址列表
    变化频繁度 | 基本不变 | 随着服务上下线而不断变更
    数据量 | 大 | 小
    数据交互/存储模型 | 消费者/提供者上报,控制台查询 | PubSub 模型,提供者上报,消费者订阅
    主要使用场景 | 服务测试、服务 | MOCK 服务调用
    可用性要求 | 元数据中心可用性要求不高,不影响主流程 | 注册中心可用性要求高,影响到服务调用的主流程

Dubbo 层次化结构

Dubbo框架
Dubbo 的架构是分层的,使用这种方式可以使各个层之间解耦合(或者最大限度地松耦合)。从服务模型的角度来看,Dubbo 采用的是一种非常简单的模型,要么是提供方提供服务,要么是消费方消费服务。

Dubbo扩展

  • 服务接口层(Service):该层是与实际业务逻辑相关的,根据服务提供方和服务消费方的业务设计对应的接口和实现。

RPC 是 Dubbo 的核心:

  • 配置层(Config)
    对外配置接口,以 ServiceConfigReferenceConfig 为中心,可以直接 new 配置类,也可以通过 Spring 解析配置生成配置类。
  • 服务代理层(Proxy)
    服务接口透明代理。Proxy 层封装了所有接口的透明化代理,而在其它层都以 Invoker 为中心,只有到了暴露给用户使用时,才用 ProxyInvoker 转成接口,或将接口实现转成 Invoker,也就是去掉 Proxy 层 RPC 是可以 Run 的,只是不那么透明,不那么看起来像调本地服务一样调远程服务。
    Proxy 层封装了所有接口的透明化代理,而在其它层都以 Invoker 为中心,只有到了暴露给用户使用时,才用 Proxy 将 Invoker 转成接口,或将接口实现转成 Invoker,也就是去掉 Proxy 层 RPC 是可以 Run 的,只是不那么透明,不那么看起来像调本地服务一样调远程服务。
  • 服务注册层(Registry)
    封装服务地址的注册与发现,以服务 URL 为中心,扩展接口为 RegistryFactoryRegistryRegistryService。可能没有服务注册中心,此时服务提供方直接暴露服务。
  • 集群层(Cluster)
    封装多个提供者的路由及负载均衡,并桥接注册中心,以 Invoker 为中心,扩展接口为 ClusterDirectoryRouterLoadBalance。将多个服务提供方组合为一个服务提供方,实现对服务消费方来透明,只需要与一个服务提供方进行交互。
  • 监控层(Monitor)
    RPC 调用次数和调用时间监控,以 Statistics 为中心,扩展接口为 MonitorFactoryMonitorMonitorService
  • 远程调用层(Protocol)
    封装 RPC 调用,扩展接口为 ProtocolInvokerExporter。Protocol 是服务域,它是 Invoker 暴露和引用的主功能入口,它负责 Invoker 的生命周期管理。Invoker 是实体域,它是 Dubbo 的核心模型,其它模型都向它靠扰,或转换成它,它代表一个可执行体,可向它发起 invoke 调用,它有可能是一个本地的实现,也可能是一个远程的实现,也可能一个集群实现。Protocol 是核心层,也就是只要有 Protocol + Invoker + Exporter 就可以完成非透明的 RPC 调用,然后在 Invoker 的主过程上 Filter 拦截点。

Registry 和 Monitor 实际上不算一层,而是一个独立的节点,只是为了全局概览,用层的方式画在一起。
Cluster 是外围概念,Cluster 的目的是将多个 Invoker 伪装成一个 Invoker,这样其它人只要关注 Protocol 层 Invoker 即可,加上 Cluster 或者去掉 Cluster 对其它层都不会造成影响,因为只有一个提供者时,是不需要 Cluster 的。

Remoting 实现是 Dubbo 协议的实现,如果你选择 RMI 协议,整个 Remoting 都不会用上,Remoting 内部再划为 Transport 传输层和 Exchange 信息交换层,Transport 层只负责单向消息传输,是对 Mina、Netty、Grizzly 的抽象,它也可以扩展 UDP 传输,而 Exchange 层是在传输层之上封装了 Request-Response 语义。

  • 交换层(Exchange):封装请求响应模式,同步转异步,以 Request 和 Response 为中心,扩展接口为ExchangerExchangeChannelExchangeClientExchangeServer
  • 网络传输层(Transport):抽象 mina 和 netty 为统一接口,以 Message 为中心,扩展接口为ChannelTransporterClientServerCodec
  • 数据序列化层(Serialize):可复用的一些工具,扩展接口为SerializationObjectInputObjectOutputThreadPool

Dubbo包结构

  • dubbo-common 公共逻辑模块,包括 Util 类和通用模型。
  • dubbo-remoting 远程通讯模块,相当于 Dubbo 协议的实现,如果 RPC 用 RMI 协议则不需要使用此包。
  • dubbo-rpc 远程调用模块,抽象各种协议,以及动态代理,只包含一对一的调用,不关心集群的管理。
  • dubbo-cluster 集群模块,将多个服务提供方伪装为一个提供方,包括:负载均衡、容错、路由等,集群的地址列表可以是静态配置的,也可以是由注册中心下发。
  • dubbo-registry 注册中心模块,基于注册中心下发地址的集群方式,以及对各种注册中心的抽象。
  • dubbo-monitor 监控模块,统计服务调用次数,调用时间的,调用链跟踪的服务。
  • dubbo-config 配置模块,是 Dubbo 对外的 API,用户通过 Config 使用 Dubbo,隐藏 Dubbo 所有细节。
  • dubbo-container 容器模块,是一个 Standalone 的容器,以简单的 Main 加载 Spring 启动,因为服务通常不需要 Tomcat/JBoss 等 Web 容器的特性,没必要用 Web 容器去加载服务。

集群 - Cluster

提供基于接口方法的透明远程过程调用,包括多协议支持,以及软负载均衡,失败容错,地址路由,动态配置等集群支持。

服务目录(Directory)

服务目录中存储了一些和服务提供者有关的信息,通过服务目录,服务消费者可获取到服务提供者的信息,比如 ip、端口、服务协议等。通过这些信息,服务消费者就可通过 Netty 等客户端进行远程调用。
服务目录与注册中心之间的区别:

  • 注册中心存储服务提供者信息,在 Dubbo 中通过 ZooKeeper 实现;
  • 服务目录是 Invoker 的集合,且这个集合中的元素会随注册中心的变化而进行动态调整。

服务目录会在客户端启动时初始化完成,并订阅注册中心的更新:
com.alibaba.dubbo.registry.support.FailbackRegistry#FailbackRegistry
com.alibaba.dubbo.registry.support.FailbackRegistry#subscribe

Directory 继承结构

Directory 接口包含了一个获取配置信息的方法 getUrl,实现该接口的类可以向外提供配置信息。Directory 有多个实现。

  • StaticDirectory
    获取一次 Invoker 列表后就不变了。
  • RegistryDirectory
    实现了 NotifyListener 接口,当注册中心服务配置发生变化后,RegistryDirectory 可收到与当前服务相关的变化,然后根据配置变更信息刷新 Invoker 列表。
    刷新 Invoker 列表代码:com.alibaba.dubbo.registry.integration.RegistryDirectory#refreshInvoker

路由(Router)

服务目录中包含多个 Invoker,需要通过路由规则来选择调用哪个,Dubbo 提供了 3 种路由实现:条件路由 ConditionRouter脚本路由 ScriptRouter标签路由 TagRouter

条件路由(ConditionRouter)

容错方案

集群容错
Dubbo 提供多种集群的容错方案,默认情况下为 Failover。
com.alibaba.dubbo.rpc.cluster.Cluster

Failover

失败自动切换,当出现失败,重试其它服务器 (该配置为默认配置)。通常用于读操作,但重试会带来更长时间的延迟。

1
2
3
4
5
6
<!--配置集群容错模式为失败自动切换 -->
<dubbo:reference cluster="failover" />
<!-- 调用queryOrder方法如果失败共调3次,重试2次,如果成功则只调1次 -->
<dubbo:reference>
<dubbo:method name="queryOrder" retries="2" />
</dubbo:reference>

通常用于幂等操作,多次调用副作用相同,譬如只读请求,Failover 使用得较多,推荐使用,但重试会带来更长延迟,应用于消费者和提供者的服务调用。

Failfast

快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录和修改数据,Failfast 使用得较多,但如果有机器正在重启,可能会出现调用失败,应用于消费者和提供者的服务调用。

Failsafe

失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作,Failsafe 使用得不多,但调用信息会丢失,应用于发送统计信息到监控中心。

Failback

失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作,使用得很少,不可靠,重启会丢失,应用于注册服务到注册中心。

Forking

并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,使用得很少,但需要浪费更多服务资源。

Broadcast

广播调用所有提供者,逐个调用,任意一台报错则报错 。通常用于通知所有提供者更新缓存或日志等本地资源信息,速度慢,任意一台报错则报错,使用得很少。

负载均衡

Random LoadBalance

随机调用(默认配置),按权重设置随机概率,在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重,使用较多,推荐使用,但重试时,可能出现瞬间压力不均。

1
2
3
4
<!-- 服务端方法基本负载均衡设置 -->
<dubbo:service interface="com.service.dubbo.queryOrder">
<dubbo:method name="queryOrder" loadbalance="roundrobin" />
</dubbo:service>

RoundRobin LoadBalance

轮循调用,按公约后的权重设置轮循比率,存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上,极端情况可能产生雪崩。

LeastActive LoadBalance

最少活跃数调用,相同活跃数的随机,活跃数指调用前后计数差,使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差(与时间有关)会越大,但不支持权重。

ConsistentHash LoadBalance

一致性 Hash,相同参数的请求总是发到同一提供者,当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。缺省只对第一个参数 Hash,如果要修改,请配置:

1
<dubbo:parameter key="hash.arguments" value="0,1" />

缺省用 160 份虚拟节点,如果要修改,请配置:

1
<dubbo:parameter key="hash.nodes" value="320" />

由于是通过哈希算法分摊调用,有可能出现调用不均匀的情况

远程通信 - Transport

提供对多种基于长连接的 NIO 框架抽象封装,包括多种线程模型,序列化,以及“请求-响应”模式的信息交换方式。
Dubbo 支持如下网络通信框架:

  • Mina
  • Netty
  • Grizzly

序列化 - Serialize

反射

通过缓存加载的 Class、setAccessible(false)去掉安全校验等来提高反射效率,或者使用反射包ReflectASM

序列化

对性能敏感,对开发体验要求不高的内部系统 thrift 或 protobuf
对开发体验敏感,性能有要求的内外部系统 hessian2
对序列化后的数据要求有良好的可读性 jackson/gson/xml
对兼容性和性能要求较高的系统 protobuf 或 kryo ,它们的性能相差不多,但是 protobuf 有个缺点就是要传输的每一个类的结构都要生成对应的 proto 文件。

Filter

ProtocolFilterWrapper#export:如果当前 protocol 不是 registry,则调用 buildInvokerChain
-> ProtocolFilterWrapper#buildInvokerChain
-> ExtensionLoader#getActivateExtension(URL url, String key, String group):获取系统自动激活的 Filter 和用户自定义的 Filter,最后合并返回

更多功能

限流

限流最好配置在 Provider 端,因为 Consumer 可能有很多个服务器实例,如果他们同时发起对同一 Provider 实例的请求可能会超出机器的处理能力上限。

1
2
3
4
5
6
7
8
<!-- 限制接口OrderService里的每个方法,服务提供者端的执行线程不超过10个 -->
<dubbo:service interface="com.bubbo.service.OrderService" executes="10" />
<!-- 限制接口OrderService里的queryOrderList方法,服务提供者端的执行线程不超过10个 -->
<dubbo:service interface="com.bubbo.service.OrderService">
<dubbo:method name="queryOrderList" executes="10" />
</dubbo:service>
<!--限制使用dubbo协议时在服务提供者端启用的连接数不超过1000个-->
<dubbo:provider protocol="dubbo" accepts="1000"/>

上述配置限制的是线程数,即并发连接数,Consumer 和 Provider 默认通过一条共享的 TCP 长连接通信,连接成功的情况下请求线程交由 IO 线程池异步读写数据,数据被反序列化后交由业务线程池处理具体业务,也就是对应的 Impl 实现类的具体方法。

服务隔离

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!--当同一个接口有多个实现时,可以通过group来隔离  -->
<!--服务提供者 -->
<dubbo:service group="ImplA" interface="com.bubbo.service.OrderService"/>
<dubbo:service group="ImplB" interface="com.bubbo.service.OrderService"/>
<!--服务调用者 -->
<dubbo:reference id="MethodA" group="ImplA" interface="com.bubbo.service.OrderService"/>
<dubbo:reference id="MethodB" group="ImplB" interface="com.bubbo.service.OrderService"/>

<!--当一个接口出现升级,新旧实现同时存在时,可以通过版本号来隔离,通常版本号隔离也用于联调阶段,不同版本号的服务无法调用,版本号相同的服务才能调用 -->
<!--服务提供者 -->
<dubbo:service interface="com.bubbo.service.OrderService" version="new2.0.0"/>
<dubbo:service interface="com.bubbo.service.OrderService" version="old1.0.0"/>
<!--服务调用者 -->
<dubbo:reference id="NewMethodA" interface="com.bubbo.service.OrderService" version="new2.0.0"/>
<dubbo:reference id="OldMethodB" interface="com.bubbo.service.OrderService" version="old1.0.0"/>

通过版本号,也可以实现消费者和提供者服务端直接连接,因为发起调用默认使用随机调用端负载均衡模式,当有多台提供者的时候,会随机选取,通常联调阶段都会调用指定服务进行联调,直连一般用在调试,开发阶段,只需要消费者和提供者 version 相同即可。

灰度发布

有三台服务器 A、B、C 要上线,现在三台服务器都是旧版本代码,那首先从 Ngnix 负载均衡列表里移除 A 服务器的配置,切断对 A 的访问,然后在 A 服务器不受新的代码,重新把 A 配置进 Ngnix 负载均衡列表。如果在线使用没有问题,则继续升级 B、C 服务器,否则回滚,恢复旧版本代码,这是针对三端(PC 端,微信端,移动端)跟网关系统的。
如果是针对子系统,譬如用户系统、订单系统等,可以通过分组 group 来实现子系统的灰度发布。服务提供者有两组,One、Two,将新版本代码 group 改为 Two,旧版本 group 还是 One,将新版本的消费者 group 改为 Two,这时请求定位到新的消费者再调用新的提供者,而且旧的消费者还是请求旧的提供者,如果线上没有问题,那就把提供者 group 为 One 的组改为 Two,并部署新代码,旧的消费者也改成 Two 并部署新代码如果有问题,那消费端和提供端都回滚到旧版本。

异步调用

Dubbo 默认情况下是同步调用的,就是调用后立刻返回,但如果消费端调用服务端创建文件并转化成 PDF 格式的文件这种在 IO 密集操作时,消费端同步调用需要等待对方转换结束才返回,很消耗性能,这时选择异步调用和回调调用更合适。

1
2
3
4
5
6
7
8
9
10
11
<!--
async="true" 异步调用,调用后不用等待,继续往下执行
onreturn ="CallBack.onreturn" 返回后调用自定义的类CallBack类的onreturn方法
onthrow="CallBack.onthrow" 调用后,提供者抛出异常后,返回调用自定义的类CallBack类的onthrow方法
-->
<!--服务调用者 -->
<dubbo:reference id="tranfromPDF" interface="com.bubbo.service.OrderService" >
<dubbo:method name="tranPDF" async="true"
onreturn ="CallBack.onreturn"
onthrow="CallBack.onthrow"/>
</dubbo:reference>

可以在 onthrow 事件里实现服务降级的方法,譬如遇到网络抖动,调用超时返回时可在 onthrow 里 return null。

  • 调用方
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Test
    public void testQueryOrder() {
    // 此时调用会立即拿到null值
    List<Order> list = this.orderService.queryOrderList();
    // 拿到Future的引用,在提供方返回结果后,结果值会被设置进Future
    Future<String> orderFuture = RpcContext.getContext().getFuture();
    try {
    // 该方法是阻塞方法,在拿到值之前一直等待,直到拿到值才会被唤醒,该方法会抛出异常,可以捕获
    String returnValue = orderFuture.get();
    } catch (InterruptedException e) {
    e.printStackTrace();
    } catch (ExecutionException e) {
    e.printStackTrace();
    }
    }
  • 回调方
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 回调接口
    interface ICallBack {
    // 第一个参数是返回值,第二个参数是原参数
    public void onreturn(String returnValue, String initParameter);

    // 第一个参数是异常,第二个参数是原参数
    public void onthrow(Throwable ex, String initParameter);
    }

    // 实现类
    class CallBackImpl implements ICallBack {
    public void onreturn(String returnValue, String initParameter) {
    // do something
    };

    public void onthrow(Throwable ex, String initParameter) {
    // do something
    };
    }

异步调用
调用方有一个用户线程池用于处理调用请求(比如 Tomcat 里那个线程池),请求被转发到 IO 线程池,由 IO 线程来发起对提供方的调用,此时 IO 线程会新建一个 Future 对象进 RpcContext,用户线程可以继续继续自己的业务逻辑,然后在需要的时候调用 Future 的 get 方法阻塞等待,而服务端只需要将结果返回给 IO 线程,由 IO 线程调用 notify 方法唤醒阻塞等待中的用户线程。

服务降级

服务降级用于在服务高峰期将次要服务降级,仅保留关键服务,从而降低系统负载、提升可用性。比如,订单列表正常情况下展示所有订单,但是如果是在网站开展秒杀之类的大促活动时,就可以降级展示当月的订单而不是所有,再其次,如果服务器宕机了,也最好展示兜底页而不是 504。

1
<dubbo:service interface="com.bubbo.service.OrderService" mock="com.dubbo.service.MonthOderMock"/>

热点缓存

1
2
3
4
5
<!--服务调用者 -->
<dubbo:reference id="queryCatalog" interface="com.bubbo.service.CatalogService">
<dubbo:method name="queryCatalog" cache="lru" />
</dubbo:reference>
<dubbo:monitor protocol="registry" />

如果查询的对象改变很少但又数据量很大的时候,如首页目录,可以避免每次都频繁调用服务端,可以设置本地缓存,加快热点数据的访问,Dubbo 的缓存类型 LRU 缓存,最近最少使用的数据会被清除,使用频繁的数据被保留,Thredlocal 缓存,当前线程的缓存,假如当前线程有多次请求,每次请求都需要相同的用户信息,那就适用,避免每次都去查询用户基本信息。

源码分析

环境配置比较简单,就是 zk->provider->consumer,在此不再赘述。

失败重试

Dubbo 中的失败重试机制比较丰富,基本考虑到常用的场景
http://dubbo.apache.org/zh-cn/docs/user/demos/fault-tolerent-strategy.html
FailoverClusterInvoker、FailfastClusterInvoker 等,以 FailoverClusterInvoker 为例:
FailoverClusterInvoker.doInvoke 重试几次,把失败的添加到 invoked 列表里
-> AbstractClusterInvoker.select 选一个可用的调用,如果是已经被选过或因为其他条件不可用则 reselect

负载均衡

http://dubbo.apache.org/zh-cn/docs/user/demos/loadbalance.html

幂等

Dubbo 没有提供幂等性检查功能,需要自定义。

限流

Dubbo 中的限流比较简单,采用的是计数器算法,单位时间内超出阈值的流量会被直接丢弃,而且只支持 PORVIDER 端的限流,而且为了让它生效还要搞复杂的 SPI 配置。
https://www.jianshu.com/p/7112a8d3d869
入口:TpsLimitFilter.invoke
-> TPSLimiter.isAllowable 为每个 Service 创建一个计数器 StatItem(粒度是整个 Service 有没有太大了)

降级

Dubbo 里的降级比较水,即调用出错就改成调用 Mock 接口,没有 Hystrix 中那么复杂的逻辑:
http://dubbo.apache.org/zh-cn/docs/user/demos/service-downgrade.html
https://www.cnblogs.com/java-zhao/p/8320519.html
入口:ReferenceConfig.createProxy 创建代理
-> ProxyFactory.getProxy
-> InvokerInvocationHandler.invoke
-> MockClusterInvoker.invoke 如果配置中有 fail 开头,则在远程调用失败后调用 doMockInvoke,大概逻辑是实例化一个 XxxServiceMock 服务然后调用

优雅停机

https://www.jianshu.com/p/6e4d1ecb0815

QA

说一下你们怎么用 Dubbo 的(考对 Dubbo 的应用能力)

说一下 Dubbo 的工作原理

Dubbo架构
描述 Registry、Consumer、Provider 之间的关系。

Dubbo 负载均衡策略和集群容错策略都有哪些

负载均衡策略和集群容错策略见上面的《集群》小节。

Dubbo 的动态代理策略

javassist,类似 CGLIB,通过继承目标类以生成代理类。

说一下服务注册(导出)过程

分本地暴露和远程暴露两种

说一下服务消费(引入)过程

服务的运行过程中,如果 ZooKeeper 挂掉了,这时还能正常请求吗?

说一下 Dubbo 协议

Dubbo 有几种容错机制

dubbo 有几种服务降级机制

dubbo 有几种服务降级机制

参考

  1. apache/incubator-dubbo
  2. Dubbo 文档
  3. Dubbo 实例 Demos
    中文版
  4. 设计 RPC 接口时,你有考虑过这些吗?
  5. 解密 Dubbo:自己动手编写 RPC 框架

启动过程

  1. 研究优雅停机时的一点思考
    kill -9kill -15的区别,SpringBoot 的停机机制。
  2. 一文聊透 Dubbo 优雅停机
  3. 一文聊透 Dubbo 优雅上线
  4. Spring-boot+Dubbo 应用启停源码分析
  5. 服务导出
  6. 服务引入

SPI

  1. Dubbo SPI
  2. 自适应拓展机制

协议

  1. 【RPC 专栏】深入理解 RPC 之协议篇
  2. Dubbo 在跨语言和协议穿透性方向的探索:支持 HTTP/2 gRPC
  3. 一文详细解读 Dubbo 中的 http 协议
  4. 聊聊 TCP 长连接和心跳那些事
  5. Dubbo 中的 URL 统一模型
  6. 研究网卡地址注册时的一点思考
  7. RFC 5234 - Augmented BNF for Syntax Specifications: ABNF
  8. 服务端经典的 C10k 问题(译)

心跳机制

  1. 一种心跳,两种设计
  2. 聊聊 TCP 长连接和心跳那些事

序列化

  1. 【RPC 专栏】深入理解 RPC 之序列化篇–总结篇
  2. 【RPC 专栏】深入理解 RPC 之序列化篇 —— Kryo
  3. 如何提高使用 Java 反射的效率?
  4. Java 序列化框架性能比较

Dubbo 支持多种协议,如下图所示:
Protocol扩展
在通信过程中,不同的服务等级一般对应着不同的服务质量,那么选择合适的协议便是一件非常重要的事情,需要根据应用的特征来选择。例如,使用 RMI 协议,一般会受到防火墙的限制,所以对于外部与内部进行通信的场景,就不要使用 RMI 协议,而是基于 HTTP 协议或者 Hessian 协议。

Hessian 协议

  • 连接个数:多连接
  • 连接方式:短连接
  • 传输协议:HTTP
  • 传输方式:同步传输
  • 序列化:Hessian 二进制序列化
  • 适用范围:传入传出参数数据包较大,提供者比消费者个数多,提供者压力较大,可传文件。
  • 适用场景:页面传输,文件传输,Hessian 是 Caucho 开源的一个 RPC 框架,其通讯效率高于 WebService 和 Java 自带的序列化,Hessian 底层采用 Http 通讯,采用 Servlet 暴露服务,Dubbo 缺省内嵌 Jetty 作为服务器实现。
1
2
3
4
5
6
7
8
9
10
11
12
<!--定义 hessian 协议 -->
<dubbo:protocol name="hessian" port="8080" server="jetty" />
<!--设置默认协议 -->
<dubbo:service protocol="hessian" />
<!--设置 service 协议 -->
<dubbo:service protocol="hessian" />

<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.33</version>
</dependency>

Http 协议

  • 连接个数:多连接
  • 连接方式:短连接
  • 传输协议:HTTP
  • 传输方式:同步传输
  • 序列化:表单序列化
  • 适用范围:传入传出参数数据包大小混合,提供者比消费者个数多,可用浏览器查看,可用表单或 URL 传入参数,暂不支持传文件。
  • 适用场景:需同时给应用程序和浏览器 JS 使用的服务。
1
2
<!--配置协议 -->
<dubbo:protocol name="http" port="8080" />

Thrift 协议

1
2
3
4
5
6
7
<dependency>
<groupId>org.apache.thrift</groupId>
<artifactId>libthrift</artifactId>
<version>0.8.0</version>
</dependency>

<dubbo:protocol name="thrift" port="3030" />

Dubbo 使用的 Thrift 和原生的 Thrift 协议不兼容,在原生协议的基础上添加了一些额外的头信息,比如 service name,magic number 等。

Rest 协议

1
2
3
4
5
6
7
8
<!-- 用rest协议在8080端口暴露服务 -->
<dubbo:protocol name="rest" port="8080"/>

<!-- 声明需要暴露的服务接口 -->
<dubbo:service interface="com.service.OrderService" ref="orderService"/>

<!-- 和本地bean一样实现服务 -->
<bean id="orderService" class="com.service.OrderServiceImpl" />

在代码中需要通过注解指定访问路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class OrderService {    
void createOrder(Order order);
}

@Path("orders") // 访问Url的相对路径
public class OrderServiceImpl implements OrderService {

@POST
@Path("create") // 访问Url的相对路径
// 将传递过来的JSON数据反序列化为Order对象
@Consumes({MediaType.APPLICATION_JSON})
public void createOrder(Order order) {
// create the order...
}
}

长连接 OR 短连接

Dubbo 协议缺省每服务每提供者每消费者使用单一长连接,如果数据量较大,可以使用多个连接。

1
2
3
4
5
6
<!-- 表示该服务使用 JVM 共享长连接 -->
<dubbo:service connections="0">
<dubbo:reference connections="0">
<!-- 表示该服务使用独立长连接 -->
<dubbo:service connections="1">
<dubbo:reference connections="1">

为什么要消费者比提供者个数多

因为 dubbo 协议采用单一长连接,假设网络为千兆网卡 3,根据测试经验数据每条连接最多只能压满 7MByte(不同的环境可能不一样),理论上 1 个服务提供者需要 20 个服务消费者才能压满网卡。

为什么不能传大包

因 dubbo 协议采用单一长连接,如果每次请求的数据包大小为 500KByte,假设网络为千兆网卡 3,每条连接最大 7MByte(不同的环境可能不一样,供参考),单个服务提供者的 TPS(每秒处理事务数)最大为:128MByte / 500KByte = 262。单个消费者调用单个服务提供者的 TPS(每秒处理事务数)最大为:7MByte / 500KByte = 14。如果能接受,可以考虑使用,否则网络将成为瓶颈。

为什么采用异步单一长连接

因为服务的现状大都是服务提供者少,通常只有几台机器,而服务的消费者多,可能整个网站都在访问该服务,比如 Morgan 的提供者只有 6 台提供者,却有上百台消费者,每天有 1.5 亿次调用,如果采用常规的 hessian 服务,服务提供者很容易就被压跨,通过单一连接,保证单一消费者不会压死提供者,长连接,减少连接握手验证等,并使用异步 IO,复用线程池,防止 C10K 问题(服务器无法服务 1w 左右的并发连接)。

1
2
3
4
5
6
<!-- 配置协议端口和服务提供方最大连接数,防止服务被压垮 -->
<dubbo:protocol name="dubbo" port="20880" accepts="1000" />
<!--配置dubbo默认协议 -->
<dubbo:provider protocol="dubbo" />
<!--配置dubbo设置服务协议 -->
<dubbo:service protocol="dubbo" />

不论一家企业做什么领域业务,登录基本都是绕不过去的功能——任何操作都必须在已经登录的前提下才能执行,我这里主要聚焦登录中分布式 Session 的设计,然后连带提一下其他方方面面。

阅读全文 »

执行异步任务最简单的方式就是通过线程来执行,因为线程本质上是操作系统的资源,应用如果不加限制地占用——最严重的情况下——将会导致系统的宕机。因此,本地线程任务主要依赖线程池来执行,线程池可以看做一种线程资源池,提供了对线程资源的调度功能。当然,提到线程就不得不提并发安全,这又是一个非常复杂的主题,水平有限,无法一一道清。

阅读全文 »

异步编程富有魅力,但是错误的使用不仅不会带来益处,还会使得系统变得难以维护、Bug 遍地,接下来我希望总结一下遇到的异步任务场景,减少以后遇到类似问题阻塞在设计上的时间。

曾经经历过因三方(传统行业)接口效率过低而导致服务不可用的情况,交流发现对方根本没有考虑在系统里加入缓存、消息队列等中间件,原因竟然是希望保证高一致性。
实际上大部分场景中,查询操作并没有保证一致性的意义,而写操作就算不能马上被看到结果也不会对体验造成太大的影响——只要最终能成功即可,这是符合BASE设计原则的。这个问题后续经排查发现原因是对方因为系统设计有问题、导致频繁大规模的接口超时,重启了后问题缓解,对方就不再追究了,非常无奈。没有不能解决的技术难题,只是有时候沟通、惰性等会阻碍问题的定位。
并发的话题真是非常的多,从最底层的硬件到高级语言 Java 中的 JUC,从最繁琐的业务系统(现在一般是微服务架构)到比较新的人工智能(如分布式机器学习),几乎无所不包,一直想爬出坑来,但是总觉得差点意思,在此我也仅仅能根据一些现成的资料总结出一些结论。

[x] 异步和非阻塞
[x] 并发和并行
[x] 并发模式 STM 介绍
[x] 并发模式 Actor 介绍

阅读全文 »

两地三中心

为什么要建设多个 IDC(数据中心)

灾备。

为什么是两地三中心(同城+跨城)

最大可能性地避免天灾人祸。
但距离过远、数据传输速度慢,同时会带来数据一致性问题。

这体现了 CAP 原理:一致性(C)与高可用(A)不可兼得。

部署方式

全量灾备例子:新浪的弹性伸缩服务(back up、单元化)
部分灾备例子:支付宝早期架构

研发

业务分级

  1. SLA、SLO、SLI
  2. 为什么要做业务分级
    便于做灾备。

SRE(现已归入 devops 体系)

  1. 监控体系
    IDC
    网络
    基础服务
    应用服务
    流量
    安全
    用户(agent、听云、白山云)
  2. 灾备
  3. fire-help
    发现(完备的监控系统、混沌工程)、恢复(回滚)、解决、复盘

数据
实时性(有些业务一定要实时,有些不需要)

怎么做

  1. 调研方案
  2. 可行性分析
  3. 形成多种方案
    • 进行技术选型
    • 成本分析(人力、金钱)
    • 风险分析
    • 形成一套方案
  4. 决策

成本预算

确定资源

  1. 金钱待遇
  2. 业务
  3. 职级(向谁汇报)

立项

  1. 计算规划
  2. 分配资源
  3. 时间节点(中期汇报、日报)
  4. 风险点

启动

  1. 安排任务
  2. 通过按计划完成任务来提升自我的影响力

验收

  1. 干系方的验收
    包括让 QA、产品经理验收。
  2. 非干系方的验收
    比如让媒体报道,提升我们公司的影响力。
  3. 形成闭环
    review 计划,看是否达到了预期。

常用操作 - 映射(Mapping)

映射的定义

映射就像数据库中的 schema ,描述了数据在每个字段内如何存储,包括文档可能具有的字段或 属性 、 每个字段的数据类型—比如 string, integer 或 date —以及 Lucene 是如何索引和存储这些字段的。
Lucene 也没有映射的概念,映射是 Elasticsearch 将复杂 JSON 文档 映射 成 Lucene 需要的扁平化数据的方式。

  • 比如下面的索引名叫 data,其中定义了 people 和 transactions 类型:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    {
    "data": {
    "mappings": {
    "people": {
    "properties": {
    "name": {
    "type": "string",
    },
    "address": {
    "type": "string"
    }
    }
    },
    "transactions": {
    "properties": {
    "timestamp": {
    "type": "date",
    "format": "strict_date_optional_time"
    },
    "message": {
    "type": "string"
    }
    }
    }
    }
    }
    }
    会被转换为类似下面的映射保存:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    {
    "data": {
    "mappings": {
    "_type": {
    "type": "string",
    "index": "not_analyzed"
    },
    "name": {
    "type": "string"
    }
    "address": {
    "type": "string"
    }
    "timestamp": {
    "type": "long"
    }
    "message": {
    "type": "string"
    }
    }
    }
    }
    所以虽然创建一个文档后其类型就确定了,但是实际上这个文档所占用的空间是该索引内所有字段的总和
    所以有一条建议:一个索引中的类型应当都是相似的,他们有类似的字段,比如 man 和 woman 共享 name 属性;如果两个类型的字段集互不相同,创建一个 类型的文档后将浪费很多空间,而是应该将他们分到不同的索引中。

动态映射机制

在索引一个新的文档时,es 会自动为每个字段推断类型,这个过程称为动态映射。这意味着如果你通过引号( “123” )索引一个数字,它会被映射为 string 类型,而不是 long 。但是,如果这个域已经映射为 long ,那么 Elasticsearch 会尝试将这个字符串转化为 long ,如果无法转化,则抛出一个异常。

1
2
3
4
5
6
7
8
PUT movies
{
"mappings": {
"_doc": {
"dynamic": "false" // true, false, strict
}
}
}

如果是新增字段

  • 如果Dynamic设置为true,一旦有新增字段的文档写入,Mapping会被同时更新
  • 如果Dynamic设置为false,Mapping不会被更新,新增字段的数据无法被索引,但是信息会出现在_source中
  • 如果Dynamic被设置为strict,则文档写入失败

如果是更新字段

  • 对已有字段,一旦有数据写入,就不再支持修改字段定义
  • 如果希望改变字段类型,必须reindex重建索引

数组

ES不提供专门的数组类型,但是每个字段都可以包含多个相同类型的数值,所以ES天然是支持数组类型的。

多字段类型

有时候我们希望一个字段可以被多种方式检索,比如:

  • 通过不同语言检索
  • pinyin字段的搜索
  • 还支持为搜索和索引指定不同的analyzer

一些默认的映射

布尔型: true 或者 false | boolean
整数: 123 | long
浮点数: 123.45 | double
字符串,有效日期: 2014-09-15 | date
字符串: foo bar | string
整数 : byte, short, integer
浮点数: float

自定义映射

TODO
全文字符串域和精确值字符串域的区别
使用特定语言分析器
优化域以适应部分匹配
指定自定义数据格式

查看映射

1
2
GET /megacrp/_mapping
GET /megacrp/employee/_mapping

返回属性包括:

  • index 属性控制怎样索引字符串
    • analyzed 分析字符串再索引(全文索引),字符串且只有字符串可以取这个属性
    • not_analyzed 不分析、直接索引精确值
    • no 不索引、不能被搜索到
  • analyzer 对于 analyzed 字符串域,用 analyzer 属性指定在搜索和索引时使用的分析器,默认时 standard

更新映射

  • 可以通过更新一个映射来添加一个新域,并为其设置映射(后来版本取消了 string 类型,改成了text,要注意
  • 不能将一个存在的域从 analyzed 改为 not_analyzed。因为如果一个域的映射已经存在,那么该域的数据可能已经被索引。如果你意图修改这个域的映射,索引的数据可能会出错,不能被正常的搜索。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    PUT /gb
    {
    "mappings": {
    "testmapping" : {
    "properties" : {
    "tweetgjghjggh" : {
    "type" : "text",
    "analyzer": "english"
    },
    "date" : {
    "type" : "date"
    },
    "user_id" : {
    "type" : "long"
    }}}}}
0%