Tallate

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

Docker 是什么

  • Docker 是开源应用容器引擎,轻量级容器技术。
  • 基于 Go 语言,并遵循 Apache2.0 协议开源。
  • Docker 可以让开发者打包他们的应用、依赖包及配置文件打包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 系统上,也可以实现虚拟化。
  • 容器完全使用沙箱技术,相互之间不会有任何接口。
  • 类似于虚拟机技术(vmware、vitural),但 docker 直接运行在操作系统(Linux)上,而不是运行在虚拟机中,速度快,性能开销极低。
    Docker 支持将软件编译成一个镜像,然后在镜像中对各种软件做好配置,将镜像发布出去(Docker Hub),其他使用者可以直接使用这个镜像。 运行中的这个镜像称为容器,容器启动是非常快速的。类似 windows 里面的 ghost 操 作系统,安装好后什么都有了。
    docker容器可以理解为在沙盒中运行的进程。这个沙盒包含了该进程运行所必须的资源,包括文件系统、系统类库、shell 环境等等。但这个沙盒默认是不会运行任何程序的。你需要在沙盒中运行一个进程来启动某一个容器。这个进程是该容器的唯一进程,所以当该进程结束的时候,容器也会完全的停止。

常见应用场景

  • Web 应用的自动化打包和发布。
  • 自动化测试和持续集成、发布。
  • 在服务型环境中部署和调整数据库或其他的后台应用。
  • 从头编译或者扩展现有的 OpenShift 或 Cloud Foundry 平台来搭建自己的 PaaS 环境。

Docker 特点

Docker 是一个基于容器的应用开发、部署和运行平台,它为开发者和系统管理员们提供了一种新式的应用部署方式,具有灵活(最复杂的应用都能容器化)、轻量(容器共享一个服务器内核)、可替换的(可以在容器运行过程中更新服务器)、可移植的(本地、云上皆可)、可伸缩的(可以轻松地进行复制)、可栈化(指的是可以将多个服务部署在一起,比如用 docker-compose)的特性。
Docker is a platform for developers and sysadmins to develop, deploy, and run applications with containers. The use of Linux containers to deploy applications is called containerization. Containers are not new, but their use for easily deploying applications is.
Containerization is increasingly popular because containers are:

  • Flexible: Even the most complex applications can be containerized.
  • Lightweight: Containers leverage and share the host kernel.
  • Interchangeable: You can deploy updates and upgrades on-the-fly.
  • Portable: You can build locally, deploy to the cloud, and run anywhere.
  • Scalable: You can increase and automatically distribute container replicas.
  • Stackable: You can stack services vertically and on-the-fly.

Docker 优势

容器技术相比虚拟机,主要优势在于性能上,其性能优势可以说达到了一个量级的差距。根据 Boden Russell 在 OpenStack 上做的一次基准测试报告,一个 KVM 实例的平均内存消耗有 292MB,而一个 docker 实例的平均内存消耗在 49MB 左右。在 CPU overhead 和启动时间方面,docker 基本都比 KVM 有一个量级的优势。
目前,一个 AWS 上的 micro 实例,每小时的按需使用成本大约在一美分多一些。如果用 docker 来提供实例,那么每小时的按需使用成本很可能会做到 0.1 美分。这一点对于云经济至关重要。正如经济学家 William Stanley Jevons 的理论所呈现的,随着商品的价格越便宜,人们使用它们的场景和频率会越来越多。

  1. container 是一种部署单元,用户可以自由决定部署的范围(dev、test、production),即组织容器的方式,换句话说,容器可以简化工作流和软件的开发、部署生命周期;
  2. 可以从传统的虚拟机环境平滑过渡到裸机生产环境内;
    保证了线上线下环境的一致性。我们在线下的开发环境使用 Docker 构建好 weaapp 的镜像后,可以直接在线上使用一个镜像,保证了线上线下环境的一致性,再也不会有在线下开发环境中运行正常,而部署到线上各种错误了。
  3. 实现了模块化,提高了复用性。
    我们可以将数据库和 Tomcat 运行在不同的容器中,从某种角度来说,这也降低了模块之间的耦合性,便于拓展。比如我们要把 MySQL 替换为 oracle,只需要再构建一个 oracle 镜像并启动与 Tomcat 连接即可,非常方便。对于我们构建的镜像,在其他 app 中直接拿来用就可以了,不必重复劳动。
  4. 提高整体效率;
    极大的简化了 webapp 的部署流程。在不使用 Docker 时,我们部署 app 时,要先搭建好 app 运行所需环境,这个过程做过的人都知道多么枯燥繁琐,一不小心还出错。而有了 Docker,我们只需要直接构建一个我们 webapp 的镜像然后将其运行即可,无论在多少台服务器中部署,都是如此。再比如,使用 Docker 之前要搭建一个 WordPress 对于新手来说是有些困难的,而有了 Docker,只需要从 DockerHub 上 pull 一个 WordPress 镜像并启动就可以了,非常非常方便。
  5. 实现了虚拟化,提高硬件利用率,有了 Docker,我们可以在一台服务器上运行很多 webapp,充分利用闲置资源。
    这时候,服务器的操作系统就类似于货轮,而一个个 Docker 容器就相当于货轮上的一个个集装箱。现在大热的云服务市场,不少就用了 Docker。举个例子来说,现在我们有一台操作系统为 Ubuntu14.04 的服务器,我们构建不同版本的 ubuntu 镜像并启动,并且为不同的用户分配不同的容器。这样,用一台服务器可以虚拟出 n 个运行着不同操作系统的虚拟服务器,而对于用户来说,这些是透明的––用户则认为自己拥有一台完整的服务器。据我推测,阿里云的服务器就是这么干的。这充分利用了闲置的硬件资源。
  6. Fast
    • 传统方式慢,传统情况下,应用服务器扩容缩容步骤繁多流程冗长,从服务器申请、初始化、应用部署、测试、加入退出集群、服务器下线。比如,业务遇到突发的流量高峰时,无法进行快速的扩容,当准备好的时候可能流量高峰已经过去了。
    • 传统不稳定,代码上线发布历经多个环境,在某个环境中测试时修复了 bug,代码等无法及时同步各环境中,提升了服务上线的风险。
    • Runtime performance at near bare metal speeds (typically 97+ percent or bare metal – a few ticks shaven off for bean counters).
    • Management operations (boot, stop, start, reboot, etc.) in seconds or milliseconds.
  7. Agile
    • VM-like agility – it’s still “virtualization”.
    • Seamlessly move between virtual and bare metal environments permitting new development workflows which reduce costs (e.g. develop on VMs and move to bare metal in the “click of a button” for production).
  8. Flexible
    • Containerize a “system” (OS less the kernel).
    • Containerize “application(s)”.
  9. Lightweight
    • Just enough Operating System (JeOS); include only what you need reducing image and container bloat.
    • Minimal per container penalty which equates to greater density and hence greater returns on existing assets – imagine packing 100s or 1000s of containers on a single host node.
  10. Inexpensive
    • Open source – free – lower TCO.
    • Supported with out-of-the-box modern Linux kernels.
  11. Ecosystem
    • Growing in popularity – just checkout the google trends for docker or LXC.
    • Vibrant community and numerous 3rd party applications (1000s of prebuilt images on docker index and 100s of open source apps on github or other public sources).
  12. Cloudy
    • Various Cloud management frameworks provide support for creating and managing Linux Containers – including OpenStack my personal favorite.

Docker 劣势

既然容器技术有如此大的优势,为什么基于容器的云现在还没有成为主流?我认为主要还是安全性的问题。虚拟机可以利用来自硬件的信任机制来提升安全性,这些机制在 Intel Virtualization Technology Evolution 的演示中有详细的介绍。即使如此,虚拟机仍然被视为相对不安全,比如前一段时间 Xen(半虚拟化,在硬件层和 OS 层之间的虚拟层)爆出一个漏洞,导致 AWS 不得不大量升级自己的主机。

  1. Docker Hub(镜像管理中心)不稳定
    第一个就是很重要的 Docker Hub 的访问问题。我们知道国内访问一些海外的网站有时候会有稳定性的问题。Docker Hub 在我们的实践中就经常出现访问不了的问题。但这种访问的问题并不是持续的,而是时有时无。由于大量的成熟 Docker 映像(image)都需要从 Docker Hub 下载,很多脚本在执行到这一步时,结果很难预料。一种方案是修改缺省的 Docker Hub 地址,改为采用国内的一些镜像(mirror)。但是在没有官方认证的成熟稳定的镜像网站时,Docker 映像的更新不容易得到保证;另一种方案是自行搭建自己的 Docker Hub。但是一来这样就失去了强大的社区贡献的映像资源,二来要花费很多精力来保持更新和同步。容器技术带来的简单化,又因为映像管理而复杂化,得不偿失。
  2. 运维难度大
    第二个就是容器技术的资源管理和运维。因为容器技术本身更适于解决大规模应用场景,所以通常都是集群基础上的部署、运维,但是目前对这一系列任务的自动化处理尚无统一的或者标准的框架。如果要让 Docker 真正在实际环境中发挥最大的效能并且易于维护,就需要有很成熟稳定的资源编排(orchestration)、资源调度(scheduling)和部署(deployment)的支持,但是这方面暂时还没有很明显的最佳解决方案,所以大多数人都在摸索和搭建自己的解决方案。我们在微软开放技术内部也是在一些开源技术的基础之上,自行开发了容器在微软公有云 Azure 上的资源管理调度和部署运维的系统,传统上的开发运维和持续集成,持续部署的技术,比如 Chef,Puppet,Jenkins 等,都可以很容易的与容器技术一起工作。

镜像和容器(Images and containers)

A container is launched by running an image. An image is an executable package that includes everything needed to run an application–the code, a runtime, libraries, environment variables, and configuration files.
A container is a runtime instance of an image–what the image becomes in memory when executed (that is, an image with state, or a user process). You can see a list of your running containers with the command, docker ps, just as you would in Linux.
一个镜像是:

  • 一个只读模板,可以用来创建容器,一个镜像可以创建多个容器
  • Docker 提供了一个很简单的机制来创建和更新现有的镜像,甚至可以直接从其他人那里获取做好的镜像直接使用
    可以理解为 Java 中的类
    一个容器是:
  • 容器是从镜像创建的运行实例,也就是镜像启动后的一个实例称为容器,是独立运行的一个或一组应用。
  • docker 利用容器来运行应用,他可以被启动、开始、停止、删除,每个容器都是相互隔离的、保证安全的平台。
  • 可以把容器看做是一个简易版的 Linux(包括 root 用户权限、进程空间、用户空间和网络空间等)和运行在其中的应用程序。
  • 可以理解为 Java 中通过类创建的实例。

服务端和客户端

Docker系统有两个程序:docker服务端和docker客户端。其中docker服务端是一个服务进程,管理着所有的容器。docker客户端则扮演着docker服务端的远程控制器,可以用来控制docker的服务端进程。大部分情况下,docker服务端和客户端运行在一台机器上。
Docker开放的API与Docker的守护进程进行通信。

docker 仓库(Resoisitory)

  • 仓库是集中存放镜像文件的场所,类似 git 代码仓库等。
  • 仓库(Respository)和仓库注册服务器(Registry)是有区别的。仓库注册服务器一般存放多个仓库,每个仓库又有多个镜像,每个镜像又有不同的标签(tag)。
  • 仓库分为公开仓库(public)和私有仓库(private)两种形式。
  • 最大的公开仓库是 Docker Hub,国内的公开仓库有阿里云等。
  • 可以在本地网络创建一个私有仓库。
  • 当创建好自己的镜像后,可以通过 push 命令把它上传到公开或私有仓库。
  • 仓库的概念类似 Git,仓库注册服务器可以理解为 GitHub 这种托管服务。

Containers & virtual machines

传统的部署云服务的方式是通过虚拟机完成的,虚拟机会在宿主机上运行一个完整的操作系统、通过hypervisor来间接使用宿主机的硬件资源,实际上这远远超出了应用运行所必须的资源。而容器正相反,它在操作系统中作为进程运行,与所有其他容器共享同一内核、占用相同容量的内存空间,相对来说,会更加轻量。
A container runs natively on Linux and shares the kernel of the host machine with other containers. It runs a discrete process, taking no more memory than any other executable, making it lightweight.
By contrast, a virtual machine (VM) runs a full-blown “guest” operating system with virtualaccess to host resources through a hypervisor. In general, VMs provide an environment with more resources than most applications need.
下图是Docker(容器)和传统虚拟机之间运行架构的示意图。
Container stack example Virtual machine stack example
In reality virtualization and Docker can and are used together in modern dev-ops. Most VPS providers are running bare-metal full virtualization technologies like Xen and Docker usually runs on top of a virtualized Ubuntu instance.

Docker 与 LXC(Linux Container)

LXC利用Linux上相关技术实现容器,Docker则在如下的几个方面进行了改进:
移植性:通过抽象容器配置,容器可以实现一个平台移植到另一个平台;
镜像系统:基于AUFS的镜像系统为容器的分发带来了很多的便利,同时共同的镜像层只需要存储一份,实现高效率的存储;
版本管理:类似于GIT的版本管理理念,用户可以更方面的创建、管理镜像文件;
仓库系统:仓库系统大大降低了镜像的分发和管理的成本;
周边工具:各种现有的工具(配置管理、云平台)对Docker的支持,以及基于Docker的Pass、CI等系统,让Docker的应用更加方便和多样化。

Docker与Vagrant

两者的定位完全不同
Vagrant类似于Boot2Docker(一款运行Docker的最小内核),是一套虚拟机的管理环境,Vagrant可以在多种系统上和虚拟机软件中运行,可以在Windows。Mac等非Linux平台上为Docker支持,自身具有较好的包装性和移植性。
原生Docker自身只能运行在Linux平台上,但启动和运行的性能都比虚拟机要快,往往更适合快速开发和部署应用的场景。
Docker不是虚拟机,而是进程隔离,对于资源的消耗很少,单一开发环境下Vagrant是虚拟机上的封装,虚拟机本身会消耗资源。因此对于开发环境来讲,使用Docker是更好的选择。

开始使用Docker

查看操作系统版本

Docker是基于LXC(Linux Container)的,因此最好在Linux环境下使用。
Docker要求内核版本高于3.10(如果是Ubuntu则需要高于12.04的发行版)可以使用下面命令查看操作系统版本:

1
uname -r

安装(Ubuntu)

最好上官网下载安装最新版docker-ce,命令行下的太旧了:

1
2
sudo apt-get install docker.io
docker version

或者按照官网上的步骤:

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
# 安装插件,可以使用HTTPS来下载仓库软件
sudo apt-get update
sudo apt-get install \
apt-transport-https \
ca-certificates \
curl \
software-properties-common
# 获取GPG公钥
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
# 验证公钥是正确的
sudo apt-key fingerprint 0EBFCD88
# 设置stable仓库
sudo apt-get install software-properties-common python-software-properties # 如果缺少了add-apt-repository命令需要安装一下
sudo add-apt-repository \
"deb [arch=amd64] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) \
stable"
# 安装
sudo apt-get update
sudo apt-get install docker-ce
# 安装某个特定版本
#apt-cache madison docker-ce # 列出可用版本
#sudo apt-get install docker-ce=<VERSION>
# 运行hello-world
docker run hello-world

安装(CentOS)

1
2
3
4
5
yum update
yum install docker
systemctl start docker
systemtctl enable docker # 设定为开机启动
systemtctl stop docker

安装(Mac)

1

下载镜像

1
2
3
4
# 搜索可用的镜像,或者上http://index.docker.io/查找
docker search tutorial
# 下载镜像,在docker的镜像索引网站上面,镜像都是按照用户名/镜像名的方式来存储的。有一组比较特殊的镜像,比如ubuntu这类基础镜像,经过官方的验证,值得信任,可以直接用镜像名来检索到。
docker pull learn/tutorial

运行容器

1
2
3
4
# docker run命令有两个参数,一个是镜像名,一个是要在镜像中运行的命令。
docker run learn/tutorial echo "hello word"
# 在容器中安装一个软件,在执行apt-get 命令的时候,要带上-y参数。如果不指定-y参数的话,apt-get命令会进入交互模式,需要用户输入命令来进行确认,但在docker环境中是无法响应这种交互的
docker run learn/tutorial apt-get install -y ping

保存对容器的修改

当你对某一个容器做了修改之后(通过在容器中运行某一个命令),可以把对容器的修改保存下来,这样下次可以从保存后的最新状态运行该容器。docker中保存状态的过程称之为committing,它保存的新旧状态之间的区别,从而产生一个新的版本。

1
2
3
4
5
6
7
8
9
10
11
12
# 获得查看正在运行中的、安装完ping命令之后容器的id
docker ps -l
docker container ls --all
# 查看更详细的信息
docker inspect
# 将镜像保存为learn/ping,无需拷贝完整的id,通常来讲最开始的三至四个字母即可区分,比如94b82c71517f可以简写为94b
docker commit [CONTAINER ID] learn/ping
# 查看刚保存的镜像
docker images
# 在新的镜像中运行ping www.baidu.com,旧的镜像中没有安装所以不能运行、会返回奇怪的信息
docker run learn/tutorial ping www.baidu.com
docker run learn/tutorial ping www.baidu.com

发布镜像

1
2
3
4
# 列出本地的所有镜像
docker images
# 将某一个镜像发布到官网
docker push

设置用户

  1. 使用root用户运行
    通常我们使用Docker的时候都是使用root用户身份运行的,官方说法如下:
    1
    2
    The docker daemon binds to a Unix socket instead of a TCP port. By default that Unix socket is owned by the user root and other users can access it with sudo. For this reason, docker daemon always runs as the root user. 
    To avoid having to use sudo when you use the docker command, create a Unix group called docker and add users to it. When the docker daemon starts, it makes the ownership of the Unix socket read/writable by the docker group.
  2. 使用普通用户运行
    1
    2
    3
    sudo groupadd docker # docker组可能已经存在了
    sudo gpasswd -a ${USER} docker # 将当前用户加入docker组
    sudo systemctl restart docker # 重新启动docker服务(下面是CentOS7的命令)
    然后当前用户注销再重新登录就可以正常使用docker命令了:
    1
    docker ps

登录

要登录容器进行操作,一种办法是在运行容器的时候开放22端口到外部,然后使用ssh来连接:

1
2
docker create -it --name=容器别名 -p 20022:22 ics-image
ssh -p 20022 root@localhost

另一种办法是在运行中的容器内执行/bin/bash:

1
docker exec -it 容器名 /bin/bash

Docker Daemon

Docker迁移

将一台宿主机上的Docker环境迁移到另一台宿主机上是比较方便的,只需停止Docker服务,将整个docker存储文件复制到另外一台宿主机上,然后调整另外一台宿主机的配置即可。

Docker Hub

仓库(Repository)、注册服务器(Registry)、注册索引(Index)

仓库是存放一组关联镜像的集合,比如同一个应用的不同版本的镜像;
注册服务器是存放实际的镜像的地方;
注册索引则负责维护用户的账号,权限,搜索,标签等管理。注册服务器利用注册索引来实现认证等管理。

QA

Docker Hub

  1. docker pull老超时
    试试国内的加速:https://www.daocloud.io/mirror#accelerator-doc
    或连接VPN后试试
  2. pull或push时出现一直Waiting的情况
    网上没有找到答案,开了VPN也没啥用,最后把环境变量改回来(eval $(docker-machine env -u))就好了。
  3. 从非官方仓库(如:dl.dockerpool.com)下载镜像的时候,有时候会提示“Error:Invaild registry endpoint https://dl.docker.com:5000/v1/…”?
    Docker 自1.3.0版本往后以来,加强了对镜像安全性的验证,需要手动添加对非官方仓库的信任。
    DOCKER_OPTS=”–insecure-registry dl.dockerpool.com:5000”
    重启docker服务

参考

  1. docker-library / official-images
  2. Best practices for writing Dockerfiles
  3. 十分钟带你理解 Kubernetes 核心概念
  4. Install Docker https://docs.docker.com/install/
  5. Get Docker CE for Ubuntu
  6. Get Started
  7. 入门教程 中文
  8. Docker Hub
  9. Docker Cloud
  10. 命令行参考文档
  11. 安装后配置
  12. daemon配置

原理

  1. Docker 核心技术与实现原理
  2. Docker vs Virtualization
  3. How is Docker different from a virtual machine?
  4. KVM and Docker LXC Benchmarking with OpenStack
  5. OK, I give up. Is Docker now Moby? And what is LinuxKit?
  6. Docker 切出 Moby 背后的真实原因分析
  7. docker/libcontainer
  8. docker最新代码源码编译
  9. 如何编译docker 1.2.0版本的源码

预备知识

进程和clone系统调用

进程(Process)是运行中的程序实例,又可以称为任务(Task),进程在Linux内核中使用一个PCB来表示,主要包含运行状态、信号、进程号、父进程号、运行时间累计值、正在使用的文件(文件描述符表)、本任务的局部描述符及任务状态段信息。
不同于fork,clone系统调用允许子进程共享部分父进程的上下文,如内存空间、文件描述符表、信号等。

1
2
3
4
5
6
7
/* Prototype for the glibc wrapper function */
#define _GNU_SOURCE
#include <sched.h>
int clone(int (*fn)(void *), void *child_stack,
int flags, void *arg, ...
/* pid_t *ptid, void *newtls, pid_t *ctid */ );
/* For the prototype of the raw system call, see NOTES */

clone常用于实现多线程,因为子进程和父进程可以共享内存。
不同于fork创建的子进程会从调用的位置开始执行,clone创建的子进程会执行实参传入的fn(arg),并将实参中的arg传入。
fn(arg)返回后子进程页会终止,返回值即为子进程的exit code,当然子进程在遇到显式的exit调用或终止信号也会立刻退出。
子进程与父进程共享内存,它们不能(也不应该)使用同一个栈,因此必须使用child_stack参数指定子进程使用的栈所在的内存空间。栈是从上向下生长的,因此最好指定最顶层的一个地址。
flags的低位包含子进程退出了发送给父进程的信号,If this signal is specified as anything other than SIGCHLD, then the parent process must specify the __WALL or __WCLONE options when waiting for the child with wait(2). If no signal is specified, then the parent process is not signaled when the child terminates.
flags 还可以指定子进程和父进程间可以共享的内容,具体内容见man clone

虚拟网络设备和veth pair

Linux container 中用到一个叫做veth的东西,这是一种新的设备,专门为 container 所建。veth 从名字上来看是 Virtual ETHernet 的缩写,它的作用很简单,就是要把从一个 network namespace 发出的数据包转发到另一个 namespace。veth 设备是成对的,一个是 container 之中,另一个在 container 之外,即在真实机器上能看到的。

veth设备实现原理

VETH设备总是成对出现,送到一端请求发送的数据总是从另一端以请求接受的形式出现。创建并配置正确后,向其一端输入数据,VETH会改变数据的方向并将其送入内核网络子系统,完成数据的注入,而在另一端则能读到此数据。(Namespace,其中往veth设备上任意一端上RX到的数据,都会在另一端上以TX的方式发送出去)veth工作在L2数据链路层,veth-pair设备在转发数据包过程中并不串改数据包内容。
这里写图片描述
显然,仅有veth-pair设备,容器是无法访问网络的。因为容器发出的数据包,实质上直接进入了veth1设备的协议栈里。如果容器需要访问网络,需要使用bridge等技术,将veth1接收到的数据包通过某种方式转发出去。

VETH: Typically used when you are trying to connect two entities which would want to “get hold of” (for lack of better phrase) an interface to forward/receive frames. These entities could be containers/bridges/ovs-switch etc. Say you want to connect a docker/lxc container to OVS. You can create a veth pair and push the first interface to the docker/lxc (say, as a phys interface) and push the other interface to OVS. You cannot do this with TAP.

veth设备特点

  • veth和其它的网络设备都一样,一端连接的是内核协议栈
  • veth设备是成对出现的,另一端两个设备彼此相连
  • 一个设备收到协议栈的数据发送请求后,会将数据发送到另一个设备上去

常用命令

1
2
# 创建veth
ip link add name veth0 type veth0 peer name veth1

其他细节见Linux篇《虚拟化-网络设备》

资源隔离、namespace和cgroup

namespace的主要作用是对两个系统内的标识符的命名进行隔离,namespace有以下几种。
Linux-namespace种类
这几个flag 可以在调用clone进行进程创建的时候作为参数传入,从而实现namespace的隔离,从这个角度来说,container主要是进程角度的隔离,而不是传统的虚拟机(一些虚拟机实现同样是基于对操作系统层的虚拟化,所以应该和container是类似的,比如普通vmware),因为container底层用的是同一个内核来调度。

cgroup 是linux 内核的另外一个控制和隔离进程的特性,他分为cpu ,memory,net,io等几个子系统,从而实现对进程cpu,内存,磁盘,网络等资源使用的控制。

虚拟文件系统(VFS)和chroot

将根目录设置成另外一个目录

自制容器

docker 只是一个工具,container 技术的核心还是linux 内核的cgroup + chroot + namespace 技术。

制作image

制作自己容器,需要一个image ,可以从网上下一个,也可以自己制作,制作很简单,新装一个操作系统,安装一些需要用到的软件包,然后用tar 制作 / 目录下的压缩包,去掉一些虚拟文件系统的文件,本文用的是自己制作的centos 6.5 的image。

容器实现过程

容器实现过程可以归纳为

  1. 用clone系统调用创建子进程,传入namespace的那几个参数,实现namespace的隔离;
  2. 父进程中创建veth pair ,一个veth在自己的namespace,将另一个设置为子进程的namespace,实现container和宿主机的网络通信;
  3. 父进程创建cgroup memory和cpuset子系统,将子进程attach到cgroup子系统上,实现container 的资源限制和隔离;
  4. 子进程在自己的namespace里,设置主机名,mount proc虚拟文件系统,设置veth ip,chroot到centos 6镜像的位置, 最终将进程镜像替换成/bin/bash;
  5. 父进程调用waitpid 等待子进程退出。

容器实现

见下面的《源码》部分

实验

在解压好镜像后,该镜像根目录下可能还没有必须的命令文件及其依赖的动态链接库,主要是由于用chroot改变根目录后,原来路径下的一些文件在当前镜像内都找不到了,一般安装完操作系统后这些文件应该都安装好了,如果需要可以通过手动移动来解决

1
2
ldd /bin/bash
cp --parents /lib/x86_64-linux-gnu/libtinfo.so.5 ./

编译执行容器代码。

1
2
gcc lxc_demo.c -o lxc_demo -lcgroup
sudo ./lxc_demo

接下来分别在宿主机和容器内执行命令,可以得出一些结论:

  1. 容器和宿主机镜像可能不同,但是内核相同;
    1
    2
    3
    4
    5
    6
    7
    # (宿主机内)
    hgc@hgc-X555LD:~$ uname -r
    4.13.0-45-generic

    # (容器内)
    bash-4.4# uname -r
    4.13.0-45-generic
  2. 根目录下的文件不同;
    1
    ls /
  3. hostname,宿主机内为用户设定的主机名,容器内为mydocker,说明UTS namespace隔离成功;
    1
    hostname
  4. 网络方面有回环网络卡lo和veth1,veth1 169.254.1.2 能ping 通veth0的地址(宿主机上的veth)169.254.2.1,如果在外面加iptables 做nat 转换的话,container里面还可以和外面通信。我们看不到外面宿主机的eth0 和 eth1,说明container 的network namespace 隔离成功。
    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
    # (宿主机内)
    $ ip addr
    ......
    19: veth0@if18: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 46:c0:eb:a8:34:8d brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 169.254.1.1/30 scope global veth0
    valid_lft forever preferred_lft forever
    inet6 fe80::44c0:ebff:fea8:348d/64 scope link
    valid_lft forever preferred_lft forever

    # (容器内)网络方面有回环网络卡lo和veth1
    bash-4.4# ip addr
    1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
    valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
    valid_lft forever preferred_lft forever
    18: veth1@if19: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether ce:04:78:b6:5a:2d brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 169.254.1.2/30 scope global veth1
    valid_lft forever preferred_lft forever
    inet6 fe80::cc04:78ff:feb6:5a2d/64 scope link
    valid_lft forever preferred_lft forever
    # 尝试ping宿主机上的veth0设备
    bash-4.4# ping 169.254.1.1
    PING 169.254.1.1 (169.254.1.1) 56(84) bytes of data.
    64 bytes from 169.254.1.1: icmp_seq=1 ttl=64 time=0.101 ms
    ......
  5. 目前container 里面只有/bin/bash , 且进程号为 1,不是我们常见的init进程,或者systemd 。因为/bin/bash 为该namespace 下的第一个进程,说明我们的pid namespace隔离成功。
    1
    2
    3
    4
    5
    # 查看进程
    bash-4.4# ps -aux
    USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
    0 1 0.0 0.0 9924 3264 ? S 01:02 0:00 /bin/bash
    0 8 0.0 0.0 30524 2820 ? R+ 01:02 0:00 ps -aux
  6. mount 显示挂载的文件系统,和宿主机的不一样,说明mount namespace隔离成功。
    1
    2
    3
    4
    # 查看挂载情况(这里有点问题,原文里还有rootfs、sysfs等)
    bash-4.4# mount
    proc on /proc type proc (rw,relatime)
    ......
  7. 就cgroup的隔离情况来说,在cgroup文件系统内,memory的限制是我们设置的512M,cpu使用的是0-1号。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    hgc@hgc-X555LD:~/tools/virtualmachines/ubuntu-16.04$ cd /sys/fs/cgroup/memory/mydocker_1530580167/
    hgc@hgc-X555LD:/sys/fs/cgroup/memory/mydocker_1530580167$ cat memory.limit_in_bytes
    536870912
    hgc@hgc-X555LD:/sys/fs/cgroup/memory/mydocker_1530580167$ cd /sys/fs/cgroup/cpuset/mydocker_1530580167/
    hgc@hgc-X555LD:/sys/fs/cgroup/cpuset/mydocker_1530580167$ cat cpuset.cpus
    0-1
    hgc@hgc-X555LD:/sys/fs/cgroup/cpuset/mydocker_1530580167$ cat cpuset.mems
    0
    hgc@hgc-X555LD:/sys/fs/cgroup/cpuset/mydocker_1530580167$ top
    从下面执行情况可以看出,只有0号和1号cpu idle为0 ,其他的都接近100%,说明cgroup隔离效果是很好的。
    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
    40
    41
    42
    43
    # (宿主机内)执行top命令并点击1查看CPU负载情况
    hgc@hgc-X555LD:/sys/fs/cgroup/cpuset/mydocker_1530580167$ top
    top - 10:02:22 up 13:19, 1 user, load average: 0.74, 0.68, 0.72
    Tasks: 348 total, 1 running, 346 sleeping, 0 stopped, 1 zombi
    %Cpu0 : 5.3 us, 2.3 sy, 0.0 ni, 91.7 id, 0.0 wa, 0.0 hi, 0.7
    %Cpu1 : 8.3 us, 12.6 sy, 0.0 ni, 78.8 id, 0.0 wa, 0.0 hi, 0.3
    %Cpu2 : 4.0 us, 2.0 sy, 0.0 ni, 93.9 id, 0.0 wa, 0.0 hi, 0.0
    %Cpu3 : 3.0 us, 2.4 sy, 0.0 ni, 94.6 id, 0.0 wa, 0.0 hi, 0.0
    %Cpu4 : 7.1 us, 5.7 sy, 0.0 ni, 87.2 id, 0.0 wa, 0.0 hi, 0.0
    %Cpu5 : 4.4 us, 1.4 sy, 0.0 ni, 93.9 id, 0.3 wa, 0.0 hi, 0.0
    %Cpu6 : 3.3 us, 2.7 sy, 0.0 ni, 93.7 id, 0.0 wa, 0.0 hi, 0.3
    %Cpu7 : 3.7 us, 1.3 sy, 0.0 ni, 94.6 id, 0.0 wa, 0.0 hi, 0.3
    ......

    # (容器内)执行多个死循环任务,强行使CPU忙碌
    bash-4.4# while true; do echo > /dev/null; done &
    [1] 10
    bash-4.4# while true; do echo > /dev/null; done &
    [2] 11
    bash-4.4# while true; do echo > /dev/null; done &
    [3] 12
    bash-4.4# while true; do echo > /dev/null; done &
    [4] 13
    bash-4.4# while true; do echo > /dev/null; done &
    [5] 14
    bash-4.4# while true; do echo > /dev/null; done &
    [6] 15
    bash-4.4# while true; do echo > /dev/null; done &
    [7] 16

    # (宿主机内)同样是执行top命令并点击1,可以看到头两个CPU的负载情况有了明显变化
    hgc@hgc-X555LD:/sys/fs/cgroup/cpuset/mydocker_1530580167$ top
    top - 10:05:05 up 13:22, 1 user, load average: 4.25, 1.68, 1.06
    Tasks: 355 total, 1 running, 353 sleeping, 0 stopped, 1 zombi
    %Cpu0 : 9.9 us, 34.8 sy, 0.0 ni, 53.2 id, 2.0 wa, 0.0 hi, 0.0
    %Cpu1 : 14.4 us, 43.6 sy, 0.0 ni, 40.2 id, 1.7 wa, 0.0 hi, 0.0
    %Cpu2 : 2.2 us, 0.6 sy, 0.0 ni, 92.0 id, 0.0 wa, 0.0 hi, 5.1
    %Cpu3 : 2.6 us, 1.3 sy, 0.0 ni, 95.0 id, 0.0 wa, 0.0 hi, 1.0
    %Cpu4 : 6.4 us, 10.0 sy, 0.0 ni, 83.6 id, 0.0 wa, 0.0 hi, 0.0
    %Cpu5 : 2.3 us, 1.2 sy, 0.0 ni, 95.3 id, 0.0 wa, 0.0 hi, 1.2
    %Cpu6 : 1.7 us, 0.7 sy, 0.0 ni, 97.6 id, 0.0 wa, 0.0 hi, 0.0
    %Cpu7 : 2.0 us, 2.7 sy, 0.0 ni, 95.3 id, 0.0 wa, 0.0 hi, 0.0
    ......

源码

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
#define _GNU_SOURCE

#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/mount.h>
// 必须先安装:sudo apt-get install libcgroup-dev
#include <libcgroup.h>
#include <time.h>
#include <signal.h>
#include <string.h>
#include <fcntl.h>

#define STACK_SIZE (1024 * 1024)
#define MEMORY_LIMIT (512*1024*1024)

//const char *rootfs = "/data1/centos6/rootfs/"; //centos6 镜像位置
//const char *rootfs = "/home/hgc/tools/virtualmachines/ubuntu-16.04/"; // 镜像位置
//const char *hostname = "mydocker"; //container 主机名
static char child_stack[STACK_SIZE];
//char *const child_args[] = {
// "/bin/bash",
// NULL
//};
int pipe_fd[2]; //父子进程同步

int child_main(void *args) {
char c;
// TODO:子进程里无法使用全局变量rootfs和hostname
// 镜像位置
const char *rootfs = "/home/hgc/tools/virtualmachines/ubuntu-16.04/";
// container 主机名
const char *hostname = "mydocker";
// 在子进程(容器)内执行的任务
// TODO:因为chroot改变了根目录的位置,所以在保证目标命令存在容器内的基础上(这里是/bin/bash),
// TODO:必须保证该命令依赖的资源同样存在于容器内(动态链接库,使用ldd查看)
char *const child_args[] = {
"/bin/bash",
0
};

printf("In child process(container)\n");
chroot(rootfs); //用chroot 切换根目录
if (errno != 0) {
perror("chroot()");
exit(1);
}
// TODO:这里不能使用sizeof
//clone 调用中的 CLONE_NEWUTS起隔离主机名和域名的作用
sethostname(hostname, strlen(hostname));
if (errno != 0) {
perror("sethostname()!");
exit(1);
}
//挂载proc子系统,CLONE_NEWNS 起隔离文件系统作用
// 需要在rootfs目录下创建proc目录
mount("proc", "/proc", "proc", 0, NULL);
if (errno != 0) {
perror("Mount(proc)");
exit(1);
}
//切换的根目录
chdir("/");
close(pipe_fd[1]);
read(pipe_fd[0], &c, 1);
//设置veth1 网络
system("ip link set lo up");
system("ip link set veth1 up");
system("ip addr add 169.254.1.2/30 dev veth1");
//将子进程的镜像替换成bash
printf("[%s]\n", child_args[0]);
if (execv(child_args[0], child_args) == -1) {
perror("execv(path, argv)");
}
return 1;
}

struct cgroup *cgroup_control(pid_t pid) {
struct cgroup *cgroup = NULL;
int ret;
ret = cgroup_init();
char *cgname = malloc(19 * sizeof(char));
if (ret) {
printf("error occurs while init cgroup.\n");
return NULL;
}
time_t now_time = time(NULL);
sprintf(cgname, "mydocker_%d", (int) now_time);
printf("%s\n", cgname);
cgroup = cgroup_new_cgroup(cgname);
if (!cgroup) {
ret = ECGFAIL;
printf("Error new cgroup%s\n", cgroup_strerror(ret));
goto out;
}
//添加cgroup memory 和 cpuset子系统
struct cgroup_controller *cgc = cgroup_add_controller(cgroup, "memory");
struct cgroup_controller *cgc_cpuset = cgroup_add_controller(cgroup, "cpuset");
if (!cgc || !cgc_cpuset) {
ret = ECGINVAL;
printf("Error add controller %s\n", cgroup_strerror(ret));
goto out;
}
// 内存限制 512M
if (cgroup_add_value_uint64(cgc, "memory.limit_in_bytes", MEMORY_LIMIT)) {
printf("Error limit memory.\n");
goto out;
}
//限制只能使用0和1号cpu
if (cgroup_add_value_string(cgc_cpuset, "cpuset.cpus", "0-1")) {
printf("Error limit cpuset cpus.\n");
goto out;
}
//限制只能使用0和1块内存
// TODO:使用0-1作为参数会报错“Invalid argument”
if (cgroup_add_value_string(cgc_cpuset, "cpuset.mems", "0")) {
printf("Error limit cpuset mems.\n");
goto out;
}
ret = cgroup_create_cgroup(cgroup, 0);
if (ret) {
printf("Error create cgroup%s\n", cgroup_strerror(ret));
goto out;
}
ret = cgroup_attach_task_pid(cgroup, pid);
if (ret) {
printf("Error attach_task_pid %s\n", cgroup_strerror(ret));
goto out;
}
return cgroup;
out:
if (cgroup) {
cgroup_delete_cgroup(cgroup, 0);
cgroup_free(&cgroup);
}
return NULL;
}

int main() {
char *cmd;
printf("main process: \n");
pipe(pipe_fd);
if (errno != 0) {
perror("pipe()");
exit(1);
}
// 调用clone创建子进程,传入namespace的几个flag参数,实现namespace的隔离
// 子进程执行child_main函数,其堆栈空间使用child_stack参数指定
// clone与线程的实现息息相关:http://www.xuebuyuan.com/1422353.html
int child_pid = clone(child_main, child_stack + STACK_SIZE, \
CLONE_NEWNET | CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWIPC | CLONE_NEWUTS | SIGCHLD, NULL);
struct cgroup *cg = cgroup_control(child_pid);
// 添加veth pair,设置veth1 在子进程的的namespace,veth0 在父进程的namespace,为了实现container和宿主机之间的网络通信
// linl3 实现起来太繁琐,借用命令行工具ip 实现
system("ip link add veth0 type veth peer name veth1");
asprintf(&cmd, "ip link set veth1 netns %d", child_pid); // asprintf根据字符串长度申请足够的内存空间,但在之后必须手动释放
system(cmd);
system("ip link set veth0 up");
system("ip addr add 169.254.1.1/30 dev veth0");
free(cmd);
//等执行以上命令,通知子进程,子进程设置自己的网络
close(pipe_fd[1]);
waitpid(child_pid, NULL, 0);
if (cg) {
cgroup_delete_cgroup(cg, 0); //删除cgroup 子系统
}
printf("child process exited.\n");
return 0;
}

参考

LXC

  1. Linux Containers
  2. 理解 chroot

虚拟网络设备

  1. Linux-虚拟网络设备-veth pair

clone

  1. linux的Clone()函数详解

Linux namespace

  1. Linux Namespaces机制
  2. 介绍 Linux 的命名空间

基于注册中心目录服务,使服务消费方能动态的查找服务提供方,使地址透明,不再需要写死服务提供方地址,注册中心基于接口名查询服务提供者的 IP 地址,使服务提供方可以平滑增加或减少机器。

角色分类

以功能角度来说服务可以分成以下几种:

  • 服务提供者;
  • 服务消费者;
  • 服务提供者兼消费者。

注册中心分类

可以分成以下几种注册中心:

  • Simple 注册中心 点对点直连
  • Multicast 注册中心 多播
  • Zookeeper 注册中心
  • Redis 注册中心

配置

服务提供者(provider)配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 应用名称,可显示依赖关系 -->
<dubbo:application name="dubbo-order-server" />

<!-- 注册中心是ZooKeeper,也可以选择Redis做注册中心 -->
<dubbo:registry address="zookeeper://127.0.0.1:2181"
client="zkclient" />

<!-- 通过dubbo协议在注册中心(127.0.0.1表示本机)的20880端口暴露服务 -->
<dubbo:protocol name="dubbo" host="127.0.0.1" port="20880" />

<!-- 提供服务用地的是service标签,将该接口暴露到dubbo中 -->
<dubbo:service interface="com.dubbo.service.OrderService"
ref="orderService" />

<!-- Spring容器加载具体的实现类-->
<bean id="orderService" class="dubbo.service.impl.OrderServiceImpl" />

<dubbo:monitor protocol="registry" />

服务消费者(consumer)配置:

1
2
3
4
5
6
7
8
9
10
11
<!-- 应用名称,可显示依赖关系 -->
<dubbo:application name="dubbo-user-consumer" />

<!-- zookeeper作为注册中心 ,也可以选择Redis做注册中心 -->
<dubbo:registry address="zookeeper://127.0.0.1:2181"
client="zkclient" />

<dubbo:protocol host="127.0.0.1" />

<!-- 调用服务使用reference标签,从注册中心中查找服务 -->
<dubbo:reference id="orderService" interface="com.dubbo.service.OrderService" />

查看服务注册/暴露结果

Dubbo服务注册信息
Dubbo 在 ZooKeeper 中以树形结构维护服务注册信息:

  • 服务提供者启动时: 向 /dubbo/com.foo.BarService/providers 目录下写入自己的 URL 地址;
  • 服务消费者启动时: 订阅 /dubbo/com.foo.BarService/providers 目录下的提供者 URL 地址。并向 /dubbo/com.foo.BarService/consumers 目录下写入自己的 URL 地址;
  • 监控中心启动时: 订阅 /dubbo/com.foo.BarService 目录下的所有提供者和消费者 URL 地址。

ZooKeeper 启动的时候会把配置信息加载进内存并持久化到数据库,然后启动定时器脏数据检查定时器 DirtyCheckTask,分别检查消费者和提供者的地址列表缓存、消费者和提供者地址列表的数据库数据,清理不存活的消费者和提供者数据,对于缓存中的存在的消费者和提供者而数据库不存在,提供者重新注册和消费者重新订阅。

Dubbo 提供了一些异常情况下的兜底方案:

  • 当提供者出现断电等异常停机时,注册中心能自动删除提供者信息
  • 当注册中心重启时,能自动恢复注册数据,以及订阅请求
  • 当会话过期时,能自动恢复注册数据,以及订阅请求
  • 当设置 <dubbo:registry check=”false” /> 时,记录失败注册和订阅请求,后台定时重试

在了解 ZooKeeper 基础上,还可以增加一些配置来修改注册细节:
可通过 <dubbo:registry username="admin" password="1234" /> 设置 ZooKeeper 登录信息
可通过 <dubbo:registry group="dubbo" /> 设置 ZooKeeper 的根节点,不设置将使用无根树
支持 * 号通配符 <dubbo:reference group="*" version="*" /> ,可订阅服务的所有分组和所有版本的提供者

在 Provider 启动完毕后,可以登录到 ZooKeeper 上查看注册的结果:

1
2
3
4
5
6
7
8
[zk: localhost:2181(CONNECTED) 11] ls /
[dubbo, zookeeper]
[zk: localhost:2181(CONNECTED) 12] ls /dubbo
[com.alibaba.dubbo.monitor.MonitorService, com.tallate.UserServiceBo]
[zk: localhost:2181(CONNECTED) 13] ls /dubbo/com.tallate.UserServiceBo
[configurators, consumers, providers, routers]
[zk: localhost:2181(CONNECTED) 14] ls /dubbo/com.tallate.UserServiceBo/providers
[dubbo%3A%2F%2F192.168.96.194%3A20880%2Fcom.tallate.UserServiceBo%3Fanyhost%3Dtrue%26application%3DdubboProvider%26dubbo%3D2.0.2%26generic%3Dfalse%26group%3Ddubbo%26interface%3Dcom.tallate.UserServiceBo%26methods%3DsayHello%2CtestPojo%2CsayHello2%26pid%3D28129%26revision%3D1.0.0%26side%3Dprovider%26timeout%3D3000%26timestamp%3D1575202776615%26version%3D1.0.0]

服务自动发现流程

服务自动发现功能完成下面这个流程,我们接下来分点概述:

  1. 服务提供者在启动时,向注册中心注册自己提供的服务。
  2. 服务消费者在启动时,向注册中心订阅自己所需的服务。
  3. 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
  4. 服务消费者,从提供者地址列表中,基于软负载均衡算法(基于软件的负载均衡,与 F5 相对),选一台提供者进行调用,如果调用失败,再选另一台调用。

注册和注销服务(Provider 执行流程)

服务的注册与注销,是对服务提供方角色而言,大致流程如下所示:
注册和注销服务

  1. 在接口提供者初始化时,每个接口都会创建一个 Invoker 和 Exporter,Exporter 持有 Invoker 实例,通过 Invocation 中的信息就可找到对应的 Exporter 和 Invoker
  2. 同 Consumer 的过程类似,调用 Invoker 前会调用 Invoker-Filter。
  3. 调用 Invoker.invoke() 时,通过反射调用最终的服务实现执行相关逻辑。

ServiceBean 负责了服务的暴露:

  • 继承自 ServiceConfig,export 方法实现了服务暴露的逻辑;
  • 实现了 Spring 中的 InitializingBean, DisposableBean, ApplicationContextAware, ApplicationListener, BeanNameAware

启动时,ServiceBean 主要负责以下任务:

  • 生成 DubboExporter 对象并缓存起来
  • 添加过滤器和监听器支持
  • 在 zk 上注册相关信息,暴露服务,方便被感知到
  • 监听端口,等待通信的到来

Dubbo服务导出

  1. 前置工作,主要用于检查参数和组装 URL;
    ServiceBean#onApplicationEvent: 接收 Spring 上下文刷新事件后执行服务导出操作
    -> ServiceBean#export: 导出服务
    -> ProviderConfig.getExport、getDelay 获取配置,如果 export 为 false 则无法提供给其他服务调用、一般只提供给本地调试时使用,如果需要 delay 则将任务交给一个 ScheduledExecutorService 延迟执行,否则调用 doExport 暴露服务
    -> ServiceConfig.doExport 一堆配置检查
  2. 导出服务,包含导出服务到本地(JVM)和导出服务到远程两个过程;
    ServiceConfig.doExportUrls
    导出服务,Dubbo 中所有服务都通过 URL 导出,支持多协议多注册中心导出服务(遍历 ProtocolConfig 集合导出每个服务)
    AbstractInterfaceConfig#loadRegistries
    加载注册中心链接
    ServiceConfig#doExportUrlsFor1Protocol
    组装 URL,将服务注册到注册中心
    JavassistProxyFactory#getInvoker
    获取 Invoker 实例,用于接收请求
    ServiceConfig#exportLocal、DubboProtocol#export
    根据配置信息导出服务到本地或远程,远程默认取Dubbo协议
    DubboProtocol#openServer
    开始监听请求
  3. 向注册中心注册服务,用于服务发现
    Dubbo 服务注册本质是在 zk 指定目录下创建临时节点,路径是{group}/{Interface}/providers/{url}
    RegistryProtocol#register
    -> RegistryFactory#getRegistry
    -> AbstractRegistry#register

因为Dubbo一般使用ZooKeeper作为注册中心,所以完全可以利用ZooKeeper的临时节点自动删除机制来实现服务器下线自动踢出的机制。

服务订阅和取消(Consumer 执行流程)

为了满足应用系统的需求,服务消费方的可能需要从服务注册中心订阅指定的有服务提供方发布的服务,在得到通知可以使用服务时,就可以直接调用服务。反过来,如果不需要某一个服务了,可以取消该服务。
服务订阅和取消

有两种服务引入方式:

  1. 饿汉式:Spring 容器调用 ReferenceBean 的 afterPropertiesSet 方法时引用服务,可通过配置 <dubbo:reference> 的 init 属性开启。
  2. 懒汉式:ReferenceBean 对应的服务被注入到其他类中时引用

服务提供的方式有三种:

  1. 引用本地 (JVM) 服务;
  2. 通过直连方式引用远程服务;
  3. 通过注册中心引用远程服务。

不管是哪种引用方式,最后都会得到一个 Invoker 实例。如果有多个注册中心,多个服务提供者,这个时候会得到一组 Invoker 实例,此时需要通过集群管理类 Cluster 将多个 Invoker 合并成一个实例。

获取客户端Proxy:

  1. 在 Consumer 初始化的时候,会生成一个代理注册到容器中,该代理回调中持有一个 Invoker 实例,消费调用服务接口时它的 invoke() 方法会被调用。
    spring.ReferenceBean#getObject
    ReferenceConfig#createProxy
    创建代理实例,根据 url 的协议、scope 以及 injvm 等参数检测是否需要本地引用,不是本地引用的情况下默认采用Dubbo协议。
    Protocol#refer
    -> DubboProtocol#getClients 获取客户端实例,实例类型为 ExchangeClient,ExchangeClient 不具备通信能力,它需要依赖更底层的客户端实例
    -> DubboProtocol#getSharedClient 默认获取共享客户端
    -> DubboProtocol#initClient 创建客户端实例,默认为 Netty
    -> Exchangers#connect(URL url, ExchangeHandler handler)
  2. 使用 Cluster 合并 Invoker
    org.apache.dubbo.rpc.cluster.Cluster#join
    如果配置了多个 URL,则使用 Cluster 合并多个 Invoker
  3. 创建动态代理
    -> ProxyFactory#getProxy(Invoker invoker)
    常用的动态代理技术有 javassist、cglib、jdk,其中 dubbo 使用的是 javassist。

    根据早期 Dubbo 作者梁飞(http://javatar.iteye.com/blog/814426)的说法,使用 javassist 是为了性能。

Consumer端服务调用过程

Dubbo组件

调用代理类的方法

请求实际调用的是InvokerInvocationHandler.invoke

Registry & Directory

Registry 将注册信息保存到本地的Directory

启动服务时需要给一个Dubbo接口创建代理,这时需要将注册URL转换为Invoker对象:
org.apache.dubbo.registry.integration.RegistryProtocol#refer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
url = getRegistryUrl(url);
Registry registry = registryFactory.getRegistry(url);
if (RegistryService.class.equals(type)) {
return proxyFactory.getInvoker((T) registry, type, url);
}

// group="a,b" or group="*"
Map<String, String> qs = StringUtils.parseQueryString(url.getParameterAndDecoded(REFER_KEY));
String group = qs.get(GROUP_KEY);
if (group != null && group.length() > 0) {
if ((COMMA_SPLIT_PATTERN.split(group)).length > 1 || "*".equals(group)) {
return doRefer(getMergeableCluster(), registry, type, url);
}
}
return doRefer(cluster, registry, type, url);
}

引用一个服务时,会注册一个zkListener,监听注册服务的命名空间的变更情况。
org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#doSubscribe
那么服务是怎么注册的呢?其实就是上边Provider注册服务的过程。
监听到注册中心的变更后,更新本地的Invoker列表,同时删除不可用的。
org.apache.dubbo.registry.integration.RegistryDirectory#refreshInvoker

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
private void refreshInvoker(List<URL> invokerUrls) {
Assert.notNull(invokerUrls, "invokerUrls should not be null");

if (invokerUrls.size() == 1
&& invokerUrls.get(0) != null
&& EMPTY_PROTOCOL.equals(invokerUrls.get(0).getProtocol())) {
this.forbidden = true; // Forbid to access
this.invokers = Collections.emptyList();
routerChain.setInvokers(this.invokers);
destroyAllInvokers(); // Close all invokers
} else {
this.forbidden = false; // Allow to access
Map<String, Invoker<T>> oldUrlInvokerMap = this.urlInvokerMap; // local reference

...

Map<String, Invoker<T>> newUrlInvokerMap = toInvokers(invokerUrls);// Translate url list to Invoker map

...

try {
destroyUnusedInvokers(oldUrlInvokerMap, newUrlInvokerMap); // Close the unused Invoker
} catch (Exception e) {
logger.warn("destroyUnusedInvokers error. ", e);
}
}
}

Invoker使用Directory

为了服务高可用同一个服务一般会有多个应用服务器提供,要先挑选一个提供者提供服务。在服务接口消费者初始化时,接口方法和提供者 Invoker 对应关系保存在 Directory。 中,通过调用的方法名称(或方法名称+第一个参数)获取该方法对应的提供者 Invoker 列表,如注册中心设置了路由规则,对这些 Invoker 根据路由规则进行过滤。
启动时订阅某个服务:
org.apache.dubbo.registry.integration.RegistryProtocol#doRefer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
directory.setRegistry(registry);
directory.setProtocol(protocol);
// all attributes of REFER_KEY
Map<String, String> parameters = new HashMap<String, String>(directory.getUrl().getParameters());
URL subscribeUrl = new URL(CONSUMER_PROTOCOL, parameters.remove(REGISTER_IP_KEY), 0, type.getName(), parameters);
if (!ANY_VALUE.equals(url.getServiceInterface()) && url.getParameter(REGISTER_KEY, true)) {
directory.setRegisteredConsumerUrl(getRegisteredConsumerUrl(subscribeUrl, url));
registry.register(directory.getRegisteredConsumerUrl());
}
directory.buildRouterChain(subscribeUrl);
// 订阅providers、configurators、routers这几个namespace
directory.subscribe(subscribeUrl.addParameter(CATEGORY_KEY,
PROVIDERS_CATEGORY + "," + CONFIGURATORS_CATEGORY + "," + ROUTERS_CATEGORY));

// 使用Cluster组合Invoker
Invoker invoker = cluster.join(directory);
return invoker;
}

添加监听器:
org.apache.dubbo.registry.integration.RegistryDirectory#subscribe

1
2
3
4
5
6
public void subscribe(URL url) {
setConsumerUrl(url);
CONSUMER_CONFIGURATION_LISTENER.addNotifyListener(this);
serviceConfigurationListener = new ReferenceConfigurationListener(this, url);
registry.subscribe(url, this);
}

Consumer端监听服务变更事件,刷新Invoker列表:
org.apache.dubbo.registry.integration.RegistryDirectory#refreshInvoker

Registry的几种实现

  • ZooKeeperRegistry
  • RedisRegistry
    注册信息的存储,是在启动时调用的:
    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
    @Override
    public void doRegister(URL url) {
    // key = dubbo/com.package.to.InterfaceName/providers
    String key = toCategoryPath(url);
    // url的全名
    String value = url.toFullString();
    String expire = String.valueOf(System.currentTimeMillis() + expirePeriod);
    boolean success = false;
    RpcException exception = null;
    for (Map.Entry<String, JedisPool> entry : jedisPools.entrySet()) {
    JedisPool jedisPool = entry.getValue();
    try {
    Jedis jedis = jedisPool.getResource();
    try {
    // 使用hash结构,可以providers一个key下面存多个url
    jedis.hset(key, value, expire);
    jedis.publish(key, Constants.REGISTER);
    success = true;
    if (! replicate) {
    break; //  如果服务器端已同步数据,只需写入单台机器
    }
    } finally {
    jedisPool.returnResource(jedis);
    }
    } catch (Throwable t) {
    exception = new RpcException("Failed to register service to redis registry. registry: " + entry.getKey() + ", service: " + url + ", cause: " + t.getMessage(), t);
    }
    }
    if (exception != null) {
    if (success) {
    logger.warn(exception.getMessage(), exception);
    } else {
    throw exception;
    }
    }
    }
    注册信息的主动删除,进程关闭时:
    1

Directory的几种实现

  • RegistryDirectory
    保存注册中心的服务注册信息,包括routers、configurators、provider。
  • StaticDirectory
    Invoker列表是固定的。

Cluster

封装了服务降级和容错机制,比如,如果调用失败则执行其他(FailoverClusterInvoker)、仍然调用失败则降级执行 mock(MockClusterInvoker)。
调用的第一层是MockClusterInvoker

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
40
41
42
43
44
45
public Result invoke(Invocation invocation) throws RpcException {
Result result = null;

String value = directory.getUrl().getMethodParameter(invocation.getMethodName(), MOCK_KEY, Boolean.FALSE.toString()).trim();
// 没有设置mock属性或设置为false,则直接调就完了
if (value.length() == 0 || "false".equalsIgnoreCase(value)) {
//no mock
result = this.invoker.invoke(invocation);
}
// 配成force了,直接调mock方法
else if (value.startsWith("force")) {
if (logger.isWarnEnabled()) {
logger.warn("force-mock: " + invocation.getMethodName() + " force-mock enabled , url : " + directory.getUrl());
}
//force:direct mock
result = doMockInvoke(invocation, null);
}
// fail-mock的方式
else {
try {
result = this.invoker.invoke(invocation);

//fix:#4585
if(result.getException() != null && result.getException() instanceof RpcException){
RpcException rpcException= (RpcException)result.getException();
if(rpcException.isBiz()){
throw rpcException;
}else {
result = doMockInvoke(invocation, rpcException);
}
}

} catch (RpcException e) {
if (e.isBiz()) {
throw e;
}

if (logger.isWarnEnabled()) {
logger.warn("fail-mock: " + invocation.getMethodName() + " fail-mock enabled , url : " + directory.getUrl(), e);
}
result = doMockInvoke(invocation, e);
}
}
return result;
}

实际invoke调用的是父类AbstractClusterInvoker的invoke方法,这个方法的主要功能是提供负载均衡:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Result invoke(final Invocation invocation) throws RpcException {
checkWhetherDestroyed();

// binding attachments into invocation.
Map<String, String> contextAttachments = RpcContext.getContext().getAttachments();
if (contextAttachments != null && contextAttachments.size() != 0) {
((RpcInvocation) invocation).addAttachments(contextAttachments);
}

// 找到所有可调用的服务器
List<Invoker<T>> invokers = list(invocation);
// 发送时要经过负载均衡
LoadBalance loadbalance = initLoadBalance(invokers, invocation);
RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
return doInvoke(invocation, invokers, loadbalance);
}

上面的doInvoke是一个模板方法,由子类实现,默认子类是FailoverClusterInvoker,可以看到,它先通过负载均衡策略得到一个Invoker,再调用该Invoker,Invoker的默认实现是DubboInvoker,表示使用的是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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
private Result doInvoke(List<Invoker<T>> invokers,
final List<Invoker<T>> invoked,
Holder<RpcException> lastException,
final Set<String> providers,
final Invocation invocation,
final LoadBalance loadbalance,
final int totalRetries,
int retries,
Holder<Invoker<T>> lastInvoked) throws RpcException {
if (retries < totalRetries) {
checkWheatherDestoried();
invokers = list(invocation);
checkInvokers(invokers, invocation);
}

// 负载均衡
final Invoker<T> invoker = select(loadbalance, invocation, invokers, invoked);
invoked.add(invoker);
lastInvoked.value = invoker;
RpcContext.getContext().setInvokers((List) invoked);

try {
return invoker.invoke(invocation);
} catch (RpcException e) {
//业务异常不重试
if (e.isBiz()) {
throw e;
}
lastException.value = e;
} catch (Throwable e) {
lastException.value = new RpcException(e.getMessage(), e);
} finally {
providers.add(invoker.getUrl().getAddress());
}

if (--retries == 0) {
throw populateException(invokers, lastException.value, providers, invocation, totalRetries);
}

return doInvoke(invokers, invoked, lastException, providers, invocation, loadbalance, totalRetries, retries, lastInvoked);
}

Cluster的实现

  • MockClusterInvoker
    调用失败降级到mock接口;
  • BroadcastClusterInvoker
    每个Invoker都调一次,忽略了LoadBalance;
  • AvailableClusterInvoker
    把处于可用状态的Invoker都调一遍。
  • FailoverClusterInvoker
    一个Invoker失败就换个Invoker重试几次。
  • FailbackClusterInvoker
    如果调用失败就放到一个线程池中延迟5秒再发,一般用于发消息。
  • FailfastClusterInvoker
    失败立刻报错
  • FailsafeClusterInvoker
    失败就忽略,一般是用于记日志这种失败了影响也不大的场景。
  • ForkingClusterInvoker
    一次性选n个Invoker,并行调用,只要有一个调用成功就返回,线程间通过LinkedBlockingQueue通信。

LoadBalance

Cluster 层包含多个 Invoker,LoadBalance 负责从中选出一个来调用,有多种 LoadBalance 策略,比如随机选一个(RandomLoadBalance)、轮询(RoundRobinLoadBalance)、一致性hash(ConsistentHashLoadBalance)。
实例化LoadBalance:com.alibaba.dubbo.rpc.cluster.support.AbstractClusterInvoker#invoke
使用LoadBalance选择一个Invoker:com.alibaba.dubbo.rpc.cluster.support.AbstractClusterInvoker#select

LoadBalance的多种实现

  • RandomLoadBalance
    计算权重,然后根据每个Invoker的权重调一个。
  • LeastActiveLoadBalance
    找最近最不活跃的Invoker调用,如果这样的Invoker有多个,则按权重来随机选一个。
  • RoundRobinLoadBalance
    轮询
  • ConsistentHashLoadBalance
    一致性哈希,启动时会将Invoker排列在一个圆环上:
    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
    public ConsistentHashSelector(List<Invoker<T>> invokers, String methodName, int identityHashCode) {
    this.virtualInvokers = new TreeMap<Long, Invoker<T>>();
    this.identityHashCode = identityHashCode;
    URL url = invokers.get(0).getUrl();

    String[] index = Constants.COMMA_SPLIT_PATTERN.split(url.getMethodParameter(methodName, "hash.arguments", "0"));
    argumentIndex = new int[index.length];
    for (int i = 0; i < index.length; i++) {
    argumentIndex[i] = Integer.parseInt(index[i]);
    }

    int replicaNumber = url.getMethodParameter(methodName, "hash.nodes", 160);
    for (Invoker<T> invoker : invokers) {
    String address = invoker.getUrl().getAddress();
    // 多复制几个,更均匀,避免所有请求都被hash到同一个Invoker
    for (int i = 0; i < replicaNumber / 4; i++) {
    byte[] digest = md5(address + i);
    for (int h = 0; h < 4; h++) {
    long m = hash(digest, h);
    // 放入圆环上
    virtualInvokers.put(m, invoker);
    }
    }
    }
    }
    将Invoker保存到virtualInvokers上,但是virtualInvokers本身是一个HashMap,如果新来的请求不能精确hash到其中的某个Invoker怎么办?是通过tailMap找到的下一个Invoker:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    private Invoker<T> selectForKey(long hash) {
    Invoker<T> invoker;
    Long key = hash;

    if (!virtualInvokers.containsKey(key)) {
    SortedMap<Long, Invoker<T>> tailMap = virtualInvokers.tailMap(key);
    if (tailMap.isEmpty()) {
    key = virtualInvokers.firstKey();
    } else {
    key = tailMap.firstKey();
    }
    }
    invoker = virtualInvokers.get(key);
    return invoker;
    }

Filter & Invoker 层

不过,在实际网络调用之前,Dubbo还提供Filter功能,Cluster会先激活Filter链然后最终调到DubboInvoker.invoke(RpcInvocation)

  1. ConsumerContextFilter可以将请求对象Invocation添加到上下文RpcContext中,其实就是存储到一个ThreadLocal变量中。
  2. FutureFilter在调用完毕后唤醒调用者线程。
  3. 或许还会有一些自定义的Filter,比如增加线程的TraceId、打印一些调用日志之类的,Filter结束后才最终调用到DubboInvoker

DubboInvoker封装了同步和异步调用,Dubbo 实现同步和异步调用比较关键的一点就在于由谁调用 ResponseFuture 的 get 方法。同步调用模式下,由框架自身调用 ResponseFuture 的 get 方法。异步调用模式下,则由用户调用该方法。
DubboInvoker是通过Netty发送消息的,消息本身如何发送的就不多说了。

Exchange

封装了网络客户端的发送逻辑,包括:

  • HeaderExchangeChannel
    对 Request 的序列化
  • ReferenceCountExchangeClient
    无引用时自动关闭客户端
  • HeaderExchangeClient
    心跳检测

数据编码 & 发送

DubboCodec
NettyChannel#send

Provider端接受调用的过程

  1. 接收请求
    NettyClient
    请求被接收后,通过 Netty 调用链向下传递执行
    NettyHandler#messageReceived
    NettyChannel
  2. 解码
    ExchangeCodec
  3. 线程派发
    Dispatcher
    IO 线程接收请求后分发给事件处理线程执行,具体的派发逻辑在ChannelHandler中实现,比如AllChannelHandler
  4. 请求分发
    ChannelEventRunnable
    根据请求类型将请求分发给不同的ChannelHandler处理。

Provider 端响应

Consumer 端接收响应

  1. 发送完请求后阻塞
    HeaderExchangeHandler
    用户线程在发送完请求后,会调用 DefaultFutureget 方法等待响应对象的到来,这时每个DefaultFuture都会关联一个调用编号,用于在接收到响应时能对应上请求的DefaultFuture
    当响应对象到来后,IO 线程根据调用编号可以找到DefaultFuture,之后会将响应对象保存到DefaultFuture,并唤醒用户线程。

为什么使用 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 序列化框架性能比较

接口扩展策略注解 @SPI

Dubbo中的很多扩展接口,如 Protocol、Transporter、Filter 等,都是通过 JDK 的 SPI 机制实现的,也就是说这些功能都可被用户自定义的扩展所替换,接口扩展点由注解@SPI定义。
JDK 中 SPI(Service Provider Interface)的设计与策略模式如出一辙,开发者可以替换掉 Dubbo 原扩展接口的默认实现,完成自定义需求,即可以自定义实现策略。
Dubbo 在 JDK 现有 SPI 实现的基础上做了如下改进:

  1. JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
  2. 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName();获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因。
  3. 增加了对扩展点 IoC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点。

那么 Dubbo 的 SPI 机制是怎么实现的呢?以协议扩展为例,Dubbo 中协议被抽象为 Protocol 接口。

读取扩展点

ServiceConfig#protocol

1
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension()

Dubbo 使用 ExtensionLoader 实现扩展点加载。

  • ExtensionLoader#getExtensionLoader()
    获取 ExtensionLoader 实现,保证每种扩展点一个单例。
  • ExtensionLoader#getAdaptiveExtension()
    根据不同的 SPI 扩展点,即不同的 interface,生成不同的 Adaptive 实例的代码。
    -> getAdaptiveExtensionClass()
    -> getExtensionClasses()
    -> loadExtensionClasses()
    加载所有的扩展点实现,直到扩展点方法执行时才决定调用是一个扩展点实现,即从众多的实现策略中决定具体使用哪一个策略。
    ExtensionLoader 会依次从META-INF/dubbo/internal(Dubbo 内部实现)、META-INF/dubbo/(开发者自定义策略)、META-INF/services/这几个目录下读取扩展点实现,目录下的同名文件配置了对应扩展点的实现策略,调用 loadFile 来加载对应的扩展策略。
    -> loadFile(Map<String, Class<?>> extensionClasses, String dir)

生成 Adaptive 实例

  • ExtensionLoader#loadFile
    -> String fileName = dir + type.getName()
    拼接文件路径
    -> ClassLoader classLoader = findClassLoader()
    拿到 ExtensionLoader 的类加载器。
    -> Class<?> clazz = Class.forName(line, true, classLoader);
    文件每行是一个实现类的全路径名,通过反射加载并拿到具体类型。
    -> extensionClasses.put(n, clazz)
    添加到 map 里返回。
  • ExtensionLoader#cachedClasses
    -> cachedClasses.set(classes)
    添加到缓存。
  • ExtensionLoader#createAdaptiveExtensionClass
    -> ExtensionLoader#createAdaptiveExtensionClassCode
    生成 Adaptive 类。
    -> compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension()
    拿到编译接口扩展点的一个具体实现,dubbo 内部支持 jdk 和 javassist,默认是 javassist。
    -> compiler.compile(code, classLoader)
    编译代码,生成 Adaptive 实例类。

上面提到 Compiler 也是一个扩展点,同样也依赖这个流程来实例化,在运行时生成 Adaptive 实例的时候,需要生成 Compiler 接口的 Adaptive 实例,即运行生成 Adaptive 实例的时候需要先有一个 Compiler 接口的 Adaptive 实例,那这样岂不是陷入了死循环,这里就要提到显示指定 Adaptive 实例的情况。@Adaptive注解支持类级别和方法级别:

1
2
1、类级别:只能拥有一个,注解打在接口实现类上,显示的注册一个Adaptive实例,在编译期就存在,如`AdaptiveCompiler`,解决了上面的死循环问题,由`AdaptiveCompiler`依据dubbo配置决定使用哪个编译类;
2、方法级别:在运行期动态的生成Adaptive实例。

通过 URL 动态选择协议

ExtensionLoader#createAdaptiveExtensionClassCode
生成的 Protocol 的 Adaptive 实例类,依据 URL 中 protocol key-value 的值,选择对应的 Protocol 策略来暴露和引用服务。
扩展点方法调用会有 URL 参数(或是参数有 URL 成员),这样依赖的扩展点可以从 URL 拿到配置信息,所有的扩展点自己定好配置的 Key 后,配置信息从 URL 上从最外层传入,URL 在配置传递上即是一条总线。
以 dubbo+zookeeper 为例,暴露和引用远程服务都是注册在 zookeeper 上的,服务注册在 zookeeper 上本质其实是一个 URL,远程服务调用的过程中依据 URL 的 key-value 来动态决定执行 Protocol、Filter 等接口扩展点的执行策略。
下面是 Provider 端暴露 HelloService 服务时在 zookeeper 上注册的 URL,在 zookeeper 上的路径为/dubbo/com.dubbo.test.service.HelloService/providers,URL 表示了采用 dubbo 协议,接口为 com.dubbo.test.service.HelloService,方法为 say,要执行的 Filter 为 whiteFilter 等。

1
2
[zk: localhost:2181(CONNECTED) 1] Is /dubbo/com.dubbo.test.service.HelloService/providers
[dubbo://127.0.0.1:2O881/com.dubbo.test.service.HelloService?anyhost=true&application=dubbo-test-service&dubbo=2.4.10&group=test-prod&interface=com.dubbo.test.service.HelloService&methods=say&pid=21242&revision=l.0&service.filter=whiteFilter&side=providerxtamp=1495436105078&version=l.0]

缓存

  • volatile Class<~> cachedAdaptiveClass
    这个是缓存 AdaptiveClass,如果一个扩展类的类上面带有 @Adaptive 注解,那么这个类就会被缓存在这个地方,每一种类型的扩展类只有一个 AdaptiveClass,如果发现有多个,则会报错。另外,当通过 getAdaptiveExtensionClass 来获取自适应扩展类时,如果当前还没有 AdaptiveClass,则会自动创建一个(动态生成 Java 代码,再编译,典型的比如 Protocol$Adaptive 就是这么生成的)
  • Set<~> cachedWrapperClasses
    这个是缓存包装类的,Dubbo 判断一个扩展类是否是包装类比较简单,通过构造函数来判断,如果这个扩展类有一个构造函数,其中参数是当前扩展类的类型,那么就是包装类,举个例子,ProtocolFilterWrapper 就是 protocol 扩展类的包装类,因为有这个构造函数:public ProtocolFilterWrapper(Protocol protocol)
  • Map<~> cachedActivates
    这个是缓存激活的扩展类,当然,@Activate 注解还可以规定激活的条件和时机
  • Holder<~> cachedClasses
    这个是缓存 Adaptive 和 Wrapper 扩展类之外的普通扩展类

扩展类被加载后会根据一定的规则放入以上 4 个缓存中,比如带有 @Adaptive 注解的会被放入 cachedAdaptiveClass。

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 介绍

阅读全文 »
0%