0%

源码版本是 2.7.8

Dubbo 服务导出过程始于 Spring 容器发布刷新事件,Dubbo 在接收到事件后,会立即执行服务导出逻辑。整个逻辑大致可分为三个部分,第一部分是前置工作,主要用于检查参数,组装 URL。第二部分是导出服务,包含导出服务到本地 (JVM),和导出服务到远程两个过程。第三部分是向注册中心注册服务,用于服务发现。本篇文章将会对这三个部分代码进行详细的分析。

在 2.7.8 版本中,服务导出的入口已经在 DubboBootstrapApplicationListener onApplicationContextEvent方法是在 onApplicationEvent中引用的,onApplicationEvent 是一个事件响应方法,该方法会在收到 Spring 上下文刷事件后执行服务导出操作。

在DubboBootstrap.start调用中,会调用一个exportServices方法,这个方法中,会调用 export 方法,这是就开始服务导出流程了。

前置工作

export

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
public synchronized void export() {
if (bootstrap == null) {
bootstrap = DubboBootstrap.getInstance();
bootstrap.initialize();
}

checkAndUpdateSubConfigs();

//init serviceMetadata 初始化服务元数据
serviceMetadata.setVersion(getVersion());
serviceMetadata.setGroup(getGroup());
serviceMetadata.setDefaultGroup(getGroup());
serviceMetadata.setServiceType(getInterfaceClass());
serviceMetadata.setServiceInterfaceName(getInterface());
serviceMetadata.setTarget(getRef());

//如果不允许暴露,直接放过 <dubbo:provider export="false" />
if (!shouldExport()) {
return;
}
// 如果需要延迟,则延迟暴露
if (shouldDelay()) {
DELAY_EXPORT_EXECUTOR.schedule(this::doExport, getDelay(), TimeUnit.MILLISECONDS);
} else {
// 直接暴露
doExport();
}
exported();
}

ServiceConfig.export是继承重写了父类ServiceConfigBase.export方法。在export方法中,主要是以下逻辑:

  • 检测 dubbo:service 标签的 interface 属性合法性,不合法则抛出异常
  • 检测 ProviderConfig、ApplicationConfig 等核心配置类对象是否为空,若为空,则尝试从其他配置类对象中获取相应的实例。
  • 检测并处理泛化服务和普通服务类
  • 检测本地存根配置,并进行相应的处理
  • 对 ApplicationConfig、RegistryConfig 等配置类进行检测,为空则尝试创建,若无法创建则抛出异常
  • 主要是初始化服务元数据
  • 对export配置项进行检查,判断是否需要导出服务
  • 如果需要延迟,则延迟导出,否则直接导出服务
Read more »

源码版本是 2.7.8

在以前的文章中,介绍过 Java SPI机制,想了解的可以进去了解下。今天我们要讲的是 Dubbo SPI机制。

Dubbo SPI 实例

本实例参考的是dubbo官方给的官方实例。

首先定义一个接口,名称Robot。

1
2
3
public interface Robot {
void sayHello();
}

接下来定义两个实现类,分别是 OptimusPrime 和 Bumblebee。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class OptimusPrime implements Robot {

@Override
public void sayHello() {
System.out.println("Hello, I am Optimus Prime.");
}
}

public class Bumblebee implements Robot {

@Override
public void sayHello() {
System.out.println("Hello, I am Bumblebee.");
}
}

接下来在META-INF/dubbo文件夹下创建一个文件,名称为Robot 的全限定名 com.dubbo.provider.demo.Robot (根据项目实际的全限定名),文件配置内容:

1
2
optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee
Read more »

源码版本是 2.7.8

什么是注册中心?

服务治理框架中大致分为服务通信和服务管理两部分,服务管理可以分为服务注册、服务发现以及服务被热加工介入,服务提供者Provider会往注册中心注册服务,而消费者Consumer会从注册中心订阅相关服务,并不会订阅全部的服务。

dubbo-registry 模块

在dubbo中,注册中心相关的代码在dubbo-registry模块下,子模块dubbo-registry-api中定义了注册中心相关的基础代码,而在dubbo-registry-xxx模块中则定义了具体的注册中心类型实现代码,例如dubbo-registry-zookeeper模块则存放了zookeeper注册中心的实现代码。

类关系图:

dubbo-registry-api 相关实现

Read more »

源码版本是 2.7.8

Dubbo 提供一下几种负载均衡

  • RandomLoadBalance:加权随机算法
  • RoundRobinLoadBalance:加权轮询负载均衡
  • LeastActiveLoadBalance:最小活跃数负载均衡。活跃调用数越小,表明该服务提供者效率越高,单位时间内可处理更多的请求。
  • ConsistentHashLoadBalance:一致性Hash负载均衡
  • ShortestResponseLoadBalance:最短响应负载均衡

Dubbo 的负载均衡代码位于 dubbo-cluster 目录下。抽象类 AbstractLoadBalance 实现了 LoadBalance,然后dubbo提供的几种负载均衡方法,实现了AbstractLoadBalance#doSelect

负载均衡策略

1
2
3
4
5
6
@SPI(RandomLoadBalance.NAME)
public interface LoadBalance {
@Adaptive("loadbalance")
<T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;

}

负载均衡主要是从服务提供者列表中,选择一个。

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
public abstract class AbstractLoadBalance implements LoadBalance {
static int calculateWarmupWeight(int uptime, int warmup, int weight) {
int ww = (int) ((float) uptime / ((float) warmup / (float) weight));
return ww < 1 ? 1 : (ww > weight ? weight : ww);
}

@Override
public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) {
if (CollectionUtils.isEmpty(invokers)) {
return null;
}
if (invokers.size() == 1) {//当只有一个提供者时,直接返回
return invokers.get(0);
}
//选择一个服务
return doSelect(invokers, url, invocation);
}
//具体的负载均衡策略由子类来实现
protected abstract <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation);

//权重方法
protected int getWeight(Invoker<?> invoker, Invocation invocation) {
int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.WEIGHT_KEY, Constants.DEFAULT_WEIGHT);
if (weight > 0) {
long timestamp = invoker.getUrl().getParameter(Constants.REMOTE_TIMESTAMP_KEY, 0L);
if (timestamp > 0L) {
int uptime = (int) (System.currentTimeMillis() - timestamp);
int warmup = invoker.getUrl().getParameter(Constants.WARMUP_KEY, Constants.DEFAULT_WARMUP);
if (uptime > 0 && uptime < warmup) {
weight = calculateWarmupWeight(uptime, warmup, weight);
}
}
}
return weight >= 0 ? weight : 0;
}

}

RandomLoadBalance (随机选择算法)

Read more »

源码版本是 2.7.8

Dubbo 是一款高性能、轻量级基于 Java 开发的 RPC 开源框架,是阿里 SOA 服务化治理方案的核心框架。

什么是 RPC ?

PC英文全名为Remote Procedure Call,也叫远程过程调用,其实就是一个计算机通信协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。计算机通信协议有很多种,对于开发来说,很多熟悉的是HTTP协议,我这里就做个简单的比较,HTTP协议是属于应用层的,而RPC跨越了传输层和应用层。HTTP本身的三次握手协议,每发送一次请求,都会有一次建立连接的过程,就会带来一定的延迟,并且HTTP本身的报文庞大,而RPC可以按需连接,调用结束后就断掉,也可以是长链接,多个远程过程调用共享同一个链接,可以看出来RPC的效率要高于HTTP,但是相对于开发简单快速的HTTP服务,RPC服务就会显得复杂一些。

Dubbo 框架设计

先介绍下Dubbo框架的各个模块

dubbo-registry 注册中心模块

基于注册中心下发地址的集群方式,以及对各种注册中心的抽象。

Read more »

声明:如果本文有错误,希望指出。

ReentrantLock 位于 java.util.concurrent.locks 包下,它实现了 Lock 接口和 Serializable 接口。

ReentrantLock 默认非公平,但可实现公平的(构造器传true),悲观,独享,互斥,可重入,重量级锁。ReentrantLock 就是一个普通的类,它是基于 AQS(AbstractQueuedSynchronizer)来实现的。

ReetrantLock 基本用法

构造方法

1
2
3
4
5
6
7
public ReentrantLock() {
sync = new NonfairSync();
}
//判断是否开启公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

ReentrantLock 提供公平锁和非公平锁的构造方法,默认构造方法是非公平锁。

NonfairSync 和 FairSync 都是 ReentrantLock 的内部类,继承 Sync 类。

关于公平锁和非公平锁的区别,主要是在多线程情况下,获取锁的机会是否相同。

Read more »

kubernetes 是一个开源的,用于管理云平台中多个主机上的容器化的应用,Kubernetes的目标是让部署容器化的应用简单并且高效(powerful),Kubernetes提供了应用部署,规划,更新,维护的一种机制。

Kubernetes 有多种安装方式,比如麻烦无比的二进制安装方式,本篇文章主要讲解如何使用 Kubeadm 来安装 Kubernetes 集群。

前期准备工作

系统 角色 IP 主机名 内存(2G以上)
Centos 7 master 192.168.60.11 k8s-master-11.cn 2G
Centos 7 node 192.168.60.16 k8s-node-16.cn 2G
Centos 7 node 192.168.60.170 k8s-node-170.cn 2G

本文搭建的只是 Kubernetes 集群,而不是 Kubernetes 高可用集群,高可用集群需要多台 master 节点来避免 master 节点中某台服务可能宕机。下面这些前期准备,需要在所以服务器上执行一遍。由于本文使用的操作系统是 Centos,其中你是 Ubuntu 用户,请查看官方具体的操作

根据表格中的信息修改服务器主机名的,具体的主机名请根据自己实际命名修改。需要注意的是,必须保证每台服务器的主机名唯一。

1
2
3
4
5
# 修改主机名
hostnamectl set-hostname k8s-master-11.cn

# 查看主机名
hostname

向每台服务器上的 /etc/hosts 文件添加一下配置。因为只有三台服务器,我们直接修改 hosts 文件来指定,如果是很大的集群,每次添加一个节点,都要修改每台服务器的 hosts 文件,这样会很麻烦,所以可以选择使用dns去解析,但是我们这里只有几台服务器,就直接修改 hosts 文件即可。

1
2
3
192.168.60. k8s-master-252.cn
192.168.60.227 k8s-node-227.cn
192.168.60.15 k8s-node-15.cn

关闭防火墙

Read more »

本文是读《深入剖析 Kubernetes》的笔记

在很久之前写过几篇关于 Docker 的文章,当时也正处于自己接触 Docker 不久,后来一直没在实际环境中使用过。最近公司要搞一套服务,需要使用 Kubernetes 来编排所以的容器。然后就重新拾起 Docker 的内容,最近也在学习 Kubernetes,最近在看极客时间的《深入剖析Kubernetes》,感兴趣的可以从下面地址购买。

极客时间-Kubernetes

在之前知道 Docker 容器使用了 Namespace 和 Ggroups 技术,但是对于这两个东西具体是什么,还真没搞清楚过。真心推荐极客时间的这么课,还是非常不错的。

容器技术的核心功能,就是通过约束和修改进程的动态表现,从而为其创造一个”边界“。

对于Docker等大多数Linux容器来说,Cgroups是用来制造约束的主要手段,Namespace则是用来修改进程试图的主要方法

Namespace

namespace 是 Linux 内核用来隔离内核资源的方式。通过 namespace 可以让一些进程只能看到与自己相关的一部分资源,而另外一些进程也只能看到与它们自己相关的资源,这两拨进程根本就感觉不到对方的存在。具体的实现方式是把一个或多个进程的相关资源指定在同一个 namespace 中。

Linux namespaces 是对全局系统资源的一种封装隔离,使得处于不同 namespace 的进程拥有独立的全局系统资源,改变一个 namespace 中的系统资源只会影响当前 namespace 里的进程,对其他 namespace 中的进程没有影响。

使用过 Docker 的应该知道,如果我们使用 docker exec -it 进入容器,然后使用ps命令,你会看到以下信息:

Read more »

最近在测试环境登录的时候,突然出现了下面的异常。

本来这条SQL只查询一条数据的,但是在错误日志中,发现后面莫名其妙的多了一个LIMIT。第一个反应是不是这个查询的前面用错了PageHelper的分页功能,但是查看了项目中的这处查询,发现LIMIT只是在SQL里面写死了LIMIT 1。我就懵逼了,问了下同事,同事说以前项目也出现过这种抽风的现象,然后我一脸黑人问号。

最近几天大致的看了下PageHelper(文章后面给了源码地址)这个分页工具的源码。在项目中,根据PageHelper的文档,一般会使用以下前两种分页方法(官方文档也推荐这样使用,其实我本人使用最多的是第三种):

1
2
3
4
5
6
7
8
PageHelper.startPage(1, 10);
List<User> list = userMapper.selectIf(1);
//或者
PageHelper.offsetPage(1, 10);
List<User> list = userMapper.selectIf(1);

//jdk8 lambda用法
Page<User> page = PageHelper.startPage(1, 10).doSelectPage(()-> userMapper.selectGroupBy());

从上面我们可以看出,分页的数据和SQL查询是分开的,那么PageHelper是如何做到把分页数据准确的拼接到SQL后面呢?如果你在源码中点开 PageHelper 这个类,你会发现这个类是继承 PageMethod 以及实现 Dialect 接口的。在 PageMethod 中,定义了一个 protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>(); 的全局常量,然后在源码中,你会发现PageHelper只是把你的分页数据,放到了ThreadLocal LOCAL_PAGE中了,然后就没然后了。

虽然PageHelper还有别的一些设置,比如数据库方言的选择等,但是我们今天不讨论这些,如果自己感兴趣的话,可以去翻翻源码。如果仔细的看下源码,会发现一个 PageInterceptor 类,这个类实现的是 ibatisInterceptor。在拦截器中,PageHelper 判断是查询语句是否需要分页,是否需要count查询,分页数据拼接查询等操作。下面三个截图大致的说明了一些拦截器中的一些原理(作者源码注释还是挺好理解的)。



Read more »

Linux 传统的数据传输,一般需要设计到数据的4次拷贝,4次copy,其中四次copy,其中的两次用户态和内核态之间Copy需要CPU参与,两次内核态和IO设备间的copy为DMA方式不需要CPU的参与。零拷贝技术主要就是减少用户态和内核态间copy数据次数。

比如我们现在需要把实现一个场景:从一个文件中读取数据并将数据发送到另一台服务器上。从上图中看出:

  • 应用程序调用 read 方法,这里会涉及到一次上下文切换(用户态->内核态),底层采用DMA 读取磁盘文件数据,并把内容存储到内核地址空间的读取缓存。
  • 由于应用程序无法读取内核地址空间的数据,如果需要程序操作这些数据,必须吧数据从读取缓冲区拷贝到用户缓冲区。这时,read()调用返回,引发一次上下文切换(内核态->用户态),这样就把数据拷贝到用户地址空间缓冲区了。这样应用程序就可以操作这些数据了。
  • 我们的目的是需要把数据发送到另外一个服务器,调用Socket的send()方法,这时候又涉及到一次上下文的切换(用户态->内核态),同事文件数据被进行第三次拷贝,再次从用户地址空间拷贝到内核地址空间缓冲区。
  • send() 调用返回,引发第四次上下文切换,同时进行第四次的数据拷贝,通过DMA把数据从目标套接字相关的缓存区传输到协议引擎进行发送。

在上面的图中,1和4是有DMA负责,不需要CPU参与,但是过程2和3需要CPU的参与。从这样流程中,我们会发现,如果不需要再应用程序中操作文件数据,其中2和3的步骤是多余的,直接把内核态数据读取后直接拷贝到套接字相关的缓冲区,就能提升性能。这就是零拷贝技术。

零拷贝技术可以减少数据拷贝和共享总线操作的次数,消除传输数据在存储器之间不必要的中间拷贝次数,从而有效地提高数据传输效率。零拷贝技术减少了用户应用程序地址空间和操作系统内核地址空间之间因为上下文切换而带来的开销。

Java 层实现零拷贝

Read more »