how-to Documentation

Official TypeScript developer resources and documentation for hengyoush/how-to. Verified technical context for AI.

0 stars TypeScript TypeScript 1 file · ~61,145 tokens
RAW Doc

1. Project Overview & Quickstart (hengyoush/how-to)

how-to

Open-source repository hengyoush/how-to

Repository Details

Note: High-volume repository documentation is actively indexed and synchronized by YakaAI.

2. Official Technical Reference & Guides (hengyoush/hengyoush.github.io)

File: _config.yml

title: Code Power
author: 春丸
github: https://github.com/hengyoush
about: 我是一名Java程序员, 喜欢探寻技术原理.
disqus_shortname: hengyoush-github-io
google_analytics: UA-XXXXXXXX-Y
avatar_url: /static/img/avatar.jpg

enableTags: true
tagarray: [python, java]

collections:
project:
output: true
permalink: /project/:path/

social:
- icon: fa-github
link: "https://github.com/hengyoush"

share:
twitter_username: hengyoush
fb_appid: 1749788565247320

urls:
- text: About Me
url: /about/
- text: My Projects
url: /projects/
- text: XML Feed
url: /feed.xml
- text: Category
url: /category/java

url: "https://hengyoush.github.io"
baseurl: ""
paginate: 20
per_page: 20
paginate_path: "/page/:num/"
markdown: kramdown
gems:

  • jekyll-paginate
  • jekyll-gist

compress_html:
clippings: all
comments: [""]
endings: all
ignore:
envs: ['development']


File: about.md


I'm Programmer for Java. Now, I am doing more on backend.

email: [email protected]


File: projects.md


Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore.


File: _data/projects.json

[
{
"slug": "cool-project",
"name": "Cool project 1",
"image": "placeholder.jpg",
"url": "https://www.google.com",
"date": "Jan 2014",
"tags": ["Angular JS","API"]
},
{
"name": "Cool project 2",
"image": "placeholder.jpg",
"url": "#",
"date": "May 2014",
"tags": ["Python","Django"]
},
{
"name": "Cool project 3",
"image": "placeholder.jpg",
"url": "#",
"date": "June 2014",
"tags": ["HTML","JQuery","Django"]
},
{
"name": "Cool project 4",
"image": "placeholder.jpg",
"date": "Oct 2016",
"tags": ["Python","nodejs"]
},
{
"name": "Cool project 5",
"image": "placeholder.jpg",
"date": "Oct 2016",
"tags": ["Python","nodejs"]
},
{
"name": "Cool project 6",
"image": "placeholder.jpg",
"date": "Oct 2016",
"tags": ["Python","nodejs"]
}
]


File: _posts/2019-04-29-jdk-ConcurrentHashMap-1.md

[Binary file]


File: _posts/2019-04-29-jdk-ConcurrentHashMap-2.md

[Binary file]


File: _posts/2019-04-29-jdk-ConcurrentHashMap-3.md


layout: post title: "JDK源码:ConcurrentHashMap解析(三)-put解析" date: 2019-04-29 18:30:00 +0700 categories: [java, jdk]

概论

这篇文章来分析一下CHM的putVal方法.
通过putVal方法我们可以更好的了解CHM的并发机制.

源码

由于源码较长,我们将其分为三部分:

  1. 预检查及shortCut
  2. 链表遍历
  3. treeBin遍历

shortCut部分

我们先来看shortCut部分:

java
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
	Node<K,V> f; int n, i, fh; K fk; V fv;
	if (tab == null || (n = tab.length) == 0)
		tab = initTable();
	else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
		if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
			break;                   // no lock when adding to empty bin
	}
	else if ((fh = f.hash) == MOVED)
		tab = helpTransfer(tab, f);
	else if (onlyIfAbsent // check first node without acquiring lock
			 && fh == hash
			 && ((fk = f.key) == key || (fk != null && key.equals(fk)))
			 && (fv = f.val) != null)
		return fv;

分析以上代码:

  1. 检查键值不能为null.
  2. 通过hash函数获取hash值.
  3. 进入循环(此循环是为了之后出现initTable或者helpTransfer而准备的)
    1. 判断是否需要初始化table
    2. 判断是否要插入的bin为空,如果为空,直接尝试cas,如果cas成功那么退出循环结束,否则继续循环
    3. 判断当前bin是否处于MOVED状态,如果是,调用helpTransfer方法帮助transfer.
    4. 判断bin的第一个节点是否是要查找的节点,如果onlyIfAbsent为true说明只有在目标key不存在的情况下才put,如果第一个节点就是要查找的节点那么不需要put,可以直接返回.
    5. 遍历链表(接下来讲到)

链表遍历部分

接着来看链表遍历部分:

java
V oldVal = null;
synchronized (f) {
	if (tabAt(tab, i) == f) {
		if (fh >= 0) {
			binCount = 1;
			for (Node<K,V> e = f;; ++binCount) {
				K ek;
				if (e.hash == hash &&
					((ek = e.key) == key ||
					 (ek != null && key.equals(ek)))) {
					oldVal = e.val;
					if (!onlyIfAbsent)
						e.val = value;
					break;
				}
				Node<K,V> pred = e;
				if ((e = e.next) == null) {
					pred.next = new Node<K,V>(hash, key, value);
					break;
				}
			}
		}
		else if (f instanceof TreeBin) {
			Node<K,V> p;
			binCount = 2;
			if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
										   value)) != null) {
				oldVal = p.val;
				if (!onlyIfAbsent)
					p.val = value;
			}
		}
		else if (f instanceof ReservationNode)
			throw new IllegalStateException("Recursive update");
	}
}
  1. 注意到这段代码是被synchronized (f)包裹起来的,那么f是什么呢,可以看到shortCut部分中f被这样赋值了:f = tabAt(tab, i = (n - 1) & hash),由此可见f是当时bin的第一个节点.
  2. 接下来判断tabAt(tab, i) == f是为了什么呢?注意到f可能在被赋值和到synchronized (f)之间,bin的第一个节点会被移除,如果获取锁后不进行一次double-check的话,可能同步块锁住的是一个失效的节点.
    如果这里判断是false的话,会重新进行一次外层for循环.
  3. 判断f的hash值,hash值有几个种类(详见第一篇文章)
    1. 如果大于0代表是正常的节点,进入for循环,如果在for循环内找到了目标key则直接替换val,否则在末尾添加.
    2. 如果不大于0代表可能是treebin,第二个分支判断是否是treeBin,如果是进行相关逻辑
    3. 判断是否是ReservationNode,这个ReservationNode是在compute等操作时用到的.

File: _posts/2019-04-29-jdk-longadder.md

[Binary file]


File: _posts/2019-04-29-threadlocal-study.md

[Binary file]


File: _posts/2019-05-02-codefight-leetcode-322.md


layout: post title: "Leetcode-322-零钱兑换" date: 2019-05-02 13:39:00 +0700 categories: [algorithm, dynamic programming]

题目

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1.

示例1

text
Input:coins = [1, 2, 5], amount = 11
Output: 3
Explain: 11 = 5 + 5 + 1

思路

可以使用动态规划的思想来解题,使用一个数组dp[i][j],dp[i][j]的意思是使用coins[0...i]的硬币凑成总金额j所需最少的硬币个数.

递推关系:dp[i][j] = min{dp[i-1][j], (使用最大amount/coins[i]个硬币中所需最少的那一种)}

上面的式子可能不太好懂,现在来说明一下.对于总金额j,我们通过选择不同个数个coins[i]会得到不同的结果,那么我们可以选择从0个coins[i]到j/coins[i]个,假设选择了k个coins[i],那么此时的所需硬币数为:

text
dp[i - 1][j - k*coins[i]] + k

对k的所有取值中选择使硬币数最少的那个即可,这样的时间复杂度是N3,但是我们可以观察得到dp[i-1][j-coins[i]]的值即为k的取值范围为0 - (j-coins[i]) / coins[i]时的结果,那么我们就可以利用这一点使时间复杂度降低到N2.

下面是题目的代码解答

代码

java
public int coinChange(int[] coins, int amount) {
	if (coins == null || coins.length == 0 || amount == 0) return 0;
	int[][] arr = new int[coins.length][amount + 1];
	for (int i = 0; i <= amount; i++) {
		if (i % coins[0] == 0) arr[0][i] = i / coins[0];
		else arr[0][i] = Integer.MAX_VALUE;
	}
	int result = arr[0][amount];
	for (int i = 1; i < coins.length; i++) {
		for (int j = 0, k = 0; j <= amount; j++, k++) {
			if (j < coins[i]) {
				arr[i][j] = arr[i - 1][j];
			} else {
				arr[i][j] = Math.min(arr[i - 1][j] - 1, arr[i][j - coins[i]]) + 1;
			}
			if (j == amount) {
				result = Math.min(result, arr[i][j]);
			}
		}
	}
	
	if (amount != 0 && result == Integer.MAX_VALUE) {
		return -1;
	} else {
		return result;
	}
}

File: _posts/2019-05-03-go-gopl-variable.md


layout: post title: "Golang学习:变量命名与声明" date: 2019-05-03 12:32:00 +0700 categories: [golang]

命名

  1. 一个名字必须以一个字母或者下划线开头.
  2. 在包内定义的变量如果开头字母是大写的话那么它是包外可见的,如果是小写的那么它只能被包内访问.
  3. 越小的作用域使用尽量简短的变量命名.

关键字

Go语言定义了一些关键字,这些关键字不能作为变量的命名

text
break      default       func     interface   select
case       defer         go       map         struct
chan       else          goto     package     switch
const      fallthrough   if       range       type
continue   for           import   return      var

还有一些预定义的名字,作为内建数据类型或函数,可以在定义中重新使用.

text
内建常量: true false iota nil

内建类型: int int8 int16 int32 int64
          uint uint8 uint16 uint32 uint64 uintptr
          float32 float64 complex128 complex64
          bool byte rune string error

内建函数: make len cap new append copy close delete
          complex real imag
          panic recover

声明

有四个关键字可以声明变量,分别是var const type func四个,下面来分别介绍.

var关键字

声明格式:

go
var 变量名称 类型 = 表达式

其中类型和= 表达式可以省略其中一个.

如果省略的是表达式,那么该变量会根据默认的零值进行初始化(不像Java一样如果在方法内没有赋初始值就无法使用)

简短变量声明

声明格式

go
变量名 := 表达式

例如:

go
i := 100
i, j := 1, 0 // 多个变量赋值
f, err := os.Open(name) // 利用函数的返回值进行赋值

指针

一个指针的值是另一个变量的地址.可以通过&x来获取变量x的指针.
通过*p来对p指针指向的变量的值进行操作.
例如:

go
i := 1
p := &i // 声明p为变量x的指针
fmt.print(*p) // 1
*p = 2
fmt.print(*p) // 2

除了通过&运算符获取地址之外,还可以通过new函数获取地址.如下:

go
p := new(int) // 声明一个匿名的int变量,p为匿名变量的指针
*p = 2 // 通过指针p改变匿名变量的值

File: _posts/2019-05-04-codefight-leetcode-357.md

[Binary file]


File: _posts/2019-05-06-codefight-leetcode-375.md

[Binary file]


File: _posts/2019-05-13-codefight-leetcode-376.md


layout: post title: "Leetcode-376-摆动序列" date: 2019-05-13 20:43:00 +0700 categories: [algorithm, dynamic programming]

题目

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。

例如, [1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3) 是正负交替出现的。相反, [1,4,7,2,5] 和 [1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。

给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。

示例

text
输入: [1,7,4,9,2,5]
输出: 6 
解释: 整个序列均为摆动序列。

输入: [1,17,5,10,13,15,10,5,16,8]
输出: 7
解释: 这个序列包含几个长度为 7 摆动序列,其中一个可为[1,17,10,13,10,16,8]。

输入: [1,2,3,4,5,6,7,8,9]
输出: 2

思路

可以使用动态规划, 然而本题存在贪心解如下.

对于一个长度为N的数组arr,假设它的一个最大摆动序列对应原数组的下标为[a1, a2, ..., ak],其中k为此摆动序列的长度.我们可以证明任意两个相邻的ax与ay之间,不失一般性假设arr[ax] < arr[ay] (x <= z <= y), 必然存在arr[az] < arr[az + 1], 如果表达式不成立, 那么必然存在一个包含az的更长的摆动序列, 与假设不符, 故不存在这样的az.

依照这样的规律我们可以得到一个贪心解法.

代码

java
static final boolean UP = true;
static final boolean DOWN = false;
public int wiggleMaxLength(int[] nums) {
	if (nums == null || nums.length == 0) return 0;
	if (nums.length == 1) return 1;
	int i = getFirstValidIndex(nums);
	if (i == nums.length - 1) return 1;
	boolean cur = nums[i] > nums[i + 1] ? UP : DOWN;
	int result = 1;
	while (i != nums.length - 1) {
		if (cur == UP) {
			int pre = nums[i++];
			while (i != nums.length - 1 && nums[i] < pre && nums[i] >= nums[i + 1]) {
				i++;
			}
			cur = DOWN;
			result++;
		} else {
			int pre = nums[i++];
			while (i != nums.length - 1 && nums[i] > pre && nums[i] <= nums[i + 1]) {
				i++;
			}
			cur = UP;
			result++;
		}
	}
	return result;
}

public int getFirstValidIndex(int[] nums) {
	int result = 0;
	for (;;) {
		if (result == nums.length - 1 || nums[result] != nums[result + 1]) 
			break;
		result++;
		continue;
	}
	return result;
}

File: _posts/2019-06-02-kube-pod-label.md


layout: post title: "Kubernetes学习: 使用label管理pod" date: 2019-06-02 15:39:00 +0700 categories: [k8s]

困境

当pod的数量增多(我们会存在众多的服务), 并且每个服务会部署不同的版本时, 如果没有一个方便管理某一类pod的方法那将是运维地狱, 接下来我们将介绍一种通过标签--label管理pod的方法.(实际上不仅仅是pod, 其他类型的资源也可以有label)

label的基本使用

label介绍

一个pod可以有多个不同的标签, 而且不仅仅是pod, 其他类型的资源也可以有label.
针对pod我们可以设置两个标签, 分别是 app 和 rel, 代表它是哪个应用和其版本.

如何创建label

我们首先创建一个上一篇文章中yaml文件的升级版, 文件名为kubia-manual-with-labels.yaml.
内容如下:

yaml
apiVersion: v1 # 遵循v1版本的Kubernetes API
kind: Pod # 这个yaml描述的是一个Pod
metadata:
    name: kubia-manual-v2 # Pod的名称
    labels: 
      creation_method: manual
      env: prod
spec:
    containers:
    - image: docker.io/hengyoush/kubia # 创建容器使用的镜像
      name: kubia #容器的名称
      ports:
      - containerPort: 8080 # 应用监听的端口, 指定端口是展示性的
        protocol: TCP

在这里, 我们给这个pod创建了两个label, 一个是creation_method, 还有一个是env.
现在来创建pod:

text
kubectl create -f kubia-manual-with-labels.yaml

创建完成后,使用如下命令查看pod以及其label的信息:

text
kubectl get po --show-labels

输出如下:

text
NAME              READY   STATUS    RESTARTS   AGE    LABELS
kubia-manual-v2   1/1     Running   0          47s    creation_method=manual,env=prod

如果只对env这个标签感兴趣, 那么输入如下命令即可:

text
kubectl get po -L env

输出如下:

text
NAME              READY   STATUS    RESTARTS   AGE    ENV 
kubia-manual-v2   1/1     Running   0          52s    prod

修改标签

首先让我们为为一个pod添加label.
输入如下命令:

text
kubectl label po kubia-manual creation_method=manual

这样我们就为kubia-manual这个pod添加了一个label.

现在让我把kubia-manual-v2的env标签修改为debug.
输入如下命令:

text
kubectl label po kubia-manual-v2 env=debug --overwrite

现在让我们查看label修改的效果:

text
NAME              READY   STATUS    RESTARTS   AGE     LABELS
kubia-d58dn       1/1     Running   0          146m    run=kubia
kubia-manual      1/1     Running   0          43m     creation_method=manual
kubia-manual-v2   1/1     Running   0          7m13s   creation_method=manual,env=debug

标签选择器--label selector

基本用法示例

列出所有creation_method=manual的pod:

text
kubectl get po -l creation_method=manual

列出所有包含env标签的pod:

text
kubectl get po -l env

列出所有不包含env的pod(注意有''号)

text
kubectl get po -l '!env'

列出creation_method不为manual的pod:

text
kubectl get po -l 'creation_method!=manual'

列出所有env为prod和debug的pod:

text
kubectl get po -l 'env in (prod,debug)'

列出所有env不为prod和debug的pod:

text
kubectl get po -l 'env notin (prod,debug)'

使用多个条件时用“,”分割:

text
kubectl get po -l 'env notin (prod,debug)', reation_method=manual

使用标签分类工作节点

不仅仅是pod, 工作节点也可以加label.
命令如下:

text
kubectl label node minikube gpu=true

这样就给minikube这个节点加上了一个label为gpu=true的标签.

我们现在想将pod只调度到含有该节点的node上, 可以在创建pod的yaml文件中做如下改动:

yaml
apiVersion: v1 # 遵循v1版本的Kubernetes API
kind: Pod # 这个yaml描述的是一个Pod
metadata:
    name: kubia-manual # Pod的名称
spec:
    nodeSelector:
      gpu: "true"
    containers:
    - image: docker.io/hengyoush/kubia # 创建容器使用的镜像
      name: kubia #容器的名称

现在我们加上了一个nodeSelector字段, 这样它只会被调度到含有该标签值的节点上去了.


File: _posts/2019-06-02-kube-pod-start.md

[Binary file]


File: _posts/2019-06-02-kube-start.md


layout: post title: "Kubernetes学习: 使用minikube在Kubernetes上运行第一个应用" date: 2019-06-02 13:47:00 +0700 categories: [k8s, docker]

准备工作

安装docker

docker可以从官网下载, 链接地址: https://hub.docker.com/?overlay=onboarding

如果是linux可以使用apt-get或者yum直接安装.

安装VirtualVM

下载地址: https://download.virtualbox.org/virtualbox/5.2.18/VirtualBox-5.2.18-124319-OSX.dmg

安装kubectl

执行以下命令:

text
brew install kubernetes-cli

安装成功执行:

text
kubectl version

安装minikube

minikube是一个可以构建单节点Kubernetes集群的工具,对于测试Kubernetes和开发应用都非常有帮助.

(由于使用minikube构建集群时需要下载一些镜像,但是由于国内的网络环境问题,我们可以使用阿里改造后的minikube.)

在终端执行以下命令:

text
curl -Lo minikube http://kubernetes.oss-cn-hangzhou.aliyuncs.com/minikube/releases/v0.28.1/minikube-darwin-amd64 && chmod +x minikube && sudo mv minikube /usr/local/bin/

下载完成即可.

正式开始

准备一个应用

启动minikube

使用以下命令启动minikube:

text
minikube start --registry-mirror=https://registry.docker-cn.com

出现如下命令行输出可认为启动成功:

text
😄  minikube v1.1.0 on darwin (amd64)
✅  using image repository registry.cn-hangzhou.aliyuncs.com/google_containers
🔥  Creating virtualbox VM (CPUs=2, Memory=2048MB, Disk=20000MB) ...
🐳  Configuring environment for Kubernetes v1.14.2 on Docker 18.09.6
🚜  Pulling images ...
🚀  Launching Kubernetes ... 
⌛  Verifying: apiserver proxy etcd scheduler controller dns
🏄  Done! kubectl is now configured to use "minikube"

现在我们还没有在这个集群中运行任何容器,接下来让我们部署应用.

部署应用

这里我们采用一种简单的方式在Kubernetes中部署应用.
在命令行中输入以下命令:

text
kubectl run kubia --image=docker.io/hengyoush/kubia --port=8080 --generator=run/v1

可以看到如下输出:
replicationcontroller "kubia" created

这里我们使用kubectl run命令, --image指定我们需要使用的容器镜像, --port告诉k8s容器正在监听8080端口, --generator让k8s创建一个Replication Controller.

接下来输入以下命令查看容器是否成功部署:

text
kubectl get pods

如果输出如下所示,那么恭喜你,部署成功了:

text
NAME          READY     STATUS    RESTARTS   AGE
kubia-d58dn   1/1       Running   0          29s

如果status那一行是Pending, 那么稍等一会再看一下,因为镜像的下载需要相应时间.

从外部访问应用

每个pod都有自己的IP地址, 但是这个地址是集群内部的, 无法从集群外部访问, 要让其可以访问, 我们需要一个LoadBalancer类型的服务. 通过创建LoadBalancer类型的服务, 让它可以从外部访问.

运行如下命令:

text
kubectl expose rc kubia --type=LoadBalancer --name kubia-http

如果输出如下,说明创建成功:
service "kubia-http" exposed

接下来运行如下命令,查看刚刚创建的服务暴露出的ip以及端口:

text
kubectl get svc

输出如下:

text
NAME         TYPE           CLUSTER-IP    EXTERNAL-IP   PORT(S)          AGE
kubernetes   ClusterIP      10.96.0.1     <none>        443/TCP          18h
kubia-http   LoadBalancer   10.105.25.8   <pending>     8080:32055/TCP   3m

(实际上Minikube不支持LoadBalancer类型的服务, 可以运行minikube service kubia-http获取可以访问的ip和端口)

水平伸缩应用

我们现在只有一个pod副本, 我们希望把副本数扩大到三个.

输入如下命令:

text
kubectl scale rc kubia --replicas=3

输出如下:
replicationcontroller "kubia" scaled

接下来我们看一下pod的情况.

运行kubectl get pods命令输出如下:

text
NAME          READY     STATUS              RESTARTS   AGE
kubia-7rvr6   0/1       ContainerCreating   0          23s
kubia-d58dn   1/1       Running             0          22m
kubia-tkqbm   0/1       ContainerCreating   0          23s

可见kubia的pod副本增加到了三个,其中两个还在启动.

接下来我们看一下水平伸缩后负载均衡的效果:

text
➜  ~ curl http://192.168.99.104:32055/
You've hit kubia-7rvr6
➜  ~ curl http://192.168.99.104:32055/
You've hit kubia-tkqbm
➜  ~ curl http://192.168.99.104:32055/
You've hit kubia-tkqbm
➜  ~ curl http://192.168.99.104:32055/
You've hit kubia-d58dn

可以看到,请求被随机的分发到了不同的pod.

dashboard

输入如下命令, dashboard将在默认浏览器中打开:

text
minikube dashboard

File: _posts/2019-06-04-docker-concept.md

[Binary file]


File: _posts/2019-06-09-kuberbetes-concept.md

[Binary file]


File: _posts/2019-06-12-jvm-method-bind.md

[Binary file]


File: _posts/2019-07-21-jvm-monitor-concept.md


layout: post title: "JVM字节码monitorenter分析" date: 2019-07-21 20:18:00 +0700 categories: [jvm]

monitorenter

我们在讲锁时,经常会提到monitorenter, 它是对应synchronized关键字的字节码其中之一, 还有monitorexit对应同步块的退出, 下面我们来看jvm源码(OPENJDK10)中
是如何处理monitorenter的.

在阅读本文章前, 你必须具有关于JVM锁的基础知识,比如: 什么是偏向锁,什么是displaced header.

源码分析

首先找到地址最高的空闲的LockRecord:

php
CASE(_monitorenter): {
            oop lockee = STACK_OBJECT(-1);
            // derefing's lockee ought to provoke implicit null check
            CHECK_NULL(lockee);
            // find a free monitor or one already allocated for this object
            // if we find a matching object then we need a new monitor
            // since this is recursive enter
            BasicObjectLock* limit = istate->monitor_base();
            BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base();
            BasicObjectLock* entry = NULL;
            while (most_recent != limit ) { // 找到一个地址最高的空闲的lockrecord
                if (most_recent->obj() == NULL) entry = most_recent;
                else if (most_recent->obj() == lockee) break; // 重入
                most_recent++;
            }
        ...

我们来看BasicObjectLock的结构:

text
BasicObjectLock
    |
    |--lock
       |--displaced_header
    |--obj

lock中会存放加锁对象原有的mark word, obj即加锁对象.

接下来我们继续往下看:

php
if (entry != NULL) {
          entry->set_obj(lockee); // 设置这个对象
          int success = false;
          uintptr_t epoch_mask_in_place = (uintptr_t)markOopDesc::epoch_mask_in_place;

          markOop mark = lockee->mark(); // 获取markword
          intptr_t hash = (intptr_t) markOopDesc::no_hash;
          ...

接着, 将获得的lock record的_obj置为当前对象.

接下来将会进入偏向锁的逻辑中:

php
// implies UseBiasedLocking
if (mark->has_bias_pattern()) { // 可偏向, 看markword最后是不是101

先判断mark word最后是不是101.
然后判断是否是重入锁, 是否需要撤销偏向等等.

接着是一段比较复杂的位运算:

php
uintptr_t thread_ident; // 当前线程id
uintptr_t anticipated_bias_locking_value;
thread_ident = (uintptr_t)istate->thread();
anticipated_bias_locking_value =
    (((uintptr_t)lockee->klass()->prototype_header() | thread_ident) ^ (uintptr_t)mark) &
    ~((uintptr_t) markOopDesc::age_mask_in_place);

咱们将其分解来看:
lockee->klass()->prototype_header()即这个对象的klass上的markword, 这上面是没有线程id的,所以它与
thread_ident相或意味着将线程id嵌入到该类的原型markword中而保留了类中的 epoch + 分代年龄 + 偏向锁标志 + 锁标志位.
然后与对象的markword做异或运算, 这会使其中相同的bit置0, 可以想见, 当当前线程持有锁时,线程id区域一定是0.
然后又与markOopDesc::age_mask_in_place的反相与,age_mask_in_place是markword中分代年龄的掩码, 所以最后一步操作将会
使分代年龄区域置0.

接下来的代码将会使用算出的这个anticipated_bias_locking_value值进行一系列判断:

分支1:已偏向当前线程

php
if  (anticipated_bias_locking_value == 0) { // 已偏向当前线程
   // already biased towards this thread, nothing to do
   if (PrintBiasedLockingStatistics) {
   (* BiasedLocking::biased_lock_entry_count_addr())++;
   }
   success = true;
}

为什么anticipated_bias_locking_value为0就会是已偏向当前线程前面已经说过了, 在此不再赘述.
这段重入的代码逻辑十分简单, 直接将success置为true, 就返回了.(详细全部代码可在附录中查看)

分支2:类markword偏向模式关闭

由于这里已经判断过对象的markword是偏向模式的, 故此时需要撤销该对象的偏向.

php
else if ((anticipated_bias_locking_value & markOopDesc::biased_lock_mask_in_place) != 0) {
    // try revoke bias
    markOop header = lockee->klass()->prototype_header(); // Klass中也有一个Markword
    if (hash != markOopDesc::no_hash) {
    header = header->copy_set_hash(hash);
    }
    if (lockee->cas_set_mark(header, mark) == mark) {// 将对象的markword替换为class中的markword
    if (PrintBiasedLockingStatistics)
        (*BiasedLocking::revoked_lock_entry_count_addr())++;
    }
}

biased_lock_mask_in_place即最后101的掩码.

此时使用cas替换对象的markword为类中的markword.接下来由于success仍然为false, 所以这个分支会进入下面的轻量级锁逻辑.
(这里的这个轻量级锁会判断重入)

分支3:epoch不等于class中的epoch,尝试重偏向

经过上面的判断,已经可以确定类的偏向模式开启并且也没有偏向当前线程.
接下来判断epoch是否与class中的不一致, 为什么会不一致呢, 是因为Jvm有批量重偏向的机制, 在批量重偏向中会更新class中的epoch
并且会更新所有正在使用的锁对象上的epoch, 那么会出现epoch过期的情况就只有一个:该对象不是正在使用的锁对象,所以它可以被重偏向到
当前线程.

php
// epoch不等于class中的epoch,尝试重偏向
else if ((anticipated_bias_locking_value & epoch_mask_in_place) !=0) {
    // try rebias
    // 构造一个偏向当前线程的markword
    markOop new_header = (markOop) ( (intptr_t) lockee->klass()->prototype_header() | thread_ident);
    if (hash != markOopDesc::no_hash) {
    new_header = new_header->copy_set_hash(hash);
    }
    if (lockee->cas_set_mark(new_header, mark) == mark) {
    if (PrintBiasedLockingStatistics)
        (* BiasedLocking::rebiased_lock_entry_count_addr())++;
    }
    else {// CAS偏向失败,使用monitorenter进入轻量级锁逻辑
    CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
    }
    success = true;
}

该分支接下来直接返回.

分支4:线程id不一致,可能是匿名偏向

php
else {
// 构造一个匿名偏向的markword
// try to bias towards thread in case object is anonymously biased
markOop header = (markOop) ((uintptr_t) mark & ((uintptr_t)markOopDesc::biased_lock_mask_in_place |
                                                (uintptr_t)markOopDesc::age_mask_in_place |
                                                epoch_mask_in_place));
if (hash != markOopDesc::no_hash) {
    header = header->copy_set_hash(hash);
}
markOop new_header = (markOop) ((uintptr_t) header | thread_ident);
// debugging hint
DEBUG_ONLY(entry->lock()->set_displaced_header((markOop) (uintptr_t) 0xdeaddead);)
if (lockee->cas_set_mark(new_header, header) == header) {
    if (PrintBiasedLockingStatistics)
    (* BiasedLocking::anonymously_biased_lock_entry_count_addr())++;
}
else {
    CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
}
success = true;

该分支逻辑简单, cas判断是否是匿名偏向否则走轻量级锁逻辑,直接返回.

轻量级锁重入逻辑

php
// traditional lightweight locking
    if (!success) {
    markOop displaced = lockee->mark()->set_unlocked();
    entry->lock()->set_displaced_header(displaced);// lockrecord中存放原先的markword
    bool call_vm = UseHeavyMonitors;
    if (call_vm || lockee->cas_set_mark((markOop)entry, displaced) != displaced) {// 将对象的markword指向lockrecord
        // Is it simple recursive case? // 判断是不是重入,如果是,简单将displacedHeader设置为null
        if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits())) {
        entry->lock()->set_displaced_header(NULL);
        } else {
        CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
        }
    }
    }

实际上只有上面的分支2会走到这里, 为什么只有分支2呢, 因为分支3和分支4其出现cas冲突就意味着获得锁的是其他线程, 而分支2是有可能存在轻量级锁重入的情况的, 所以这里有轻量级锁重入逻辑.
这里将displaced header设置为unlocked的原因是等到解锁时,会将displaced header中的markword替换回对象头上,这时对象应该时无锁的所以直接设置为
unlocked.

在cas_set_mark((markOop)entry, displaced)中如果失败, 则意味着对象已经被锁住,这里使用is_lock_owned判断是否重入,如果是,那么直接设置displaced header为null
(在轻量级锁解锁时会解释为什么这么做),如果被其他线程持有,那么进入InterpreterRuntime::monitorenter逻辑中.

重偏向与撤销

php
if (UseBiasedLocking) {
    // Retry fast entry if bias is revoked to avoid unnecessary inflation
    ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
} else {
    ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
}

可见,如果开启了偏向锁则会进入fast_enter.

fast_enter的代码如下:

php
void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock,
                                    bool attempt_rebias, TRAPS) {
  if (UseBiasedLocking) {
    if (!SafepointSynchronize::is_at_safepoint()) {
      BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
      if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
        return;
      }
    } else {
      assert(!attempt_rebias, "can not rebias toward VM thread");
      BiasedLocking::revoke_at_safepoint(obj);
    }
    assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
  }

  slow_enter(obj, lock, THREAD);
}

如果当前不在安全点,那么会进入revoke_and_rebias方法中, 如果在安全点,那么由VMThread执行revoke_at_safepoint方法.

我们先看revoke_and_rebias方法.

php
if (mark->is_biased_anonymously() && !attempt_rebias) { // hashcode方法
    markOop biased_value       = mark;
    markOop unbiased_prototype = markOopDesc::prototype()->set_age(mark->age()); // prototype是nohash + 1(无锁)
    markOop res_mark = obj->cas_set_mark(unbiased_prototype, mark);
    if (res_mark == biased_value) { // 如果成功设置,说明没有竞争
      return BIAS_REVOKED;
    }
  }

如果是匿名偏向且不允许重偏向,那么一般是计算对象的hashcode方法, 此时直接给对象设置一个无锁的markword.

如果对象存在偏向那么走下面的逻辑:

php
else if (mark->has_bias_pattern()) {
    Klass* k = obj->klass();
    markOop prototype_header = k->prototype_header();
    if (!prototype_header->has_bias_pattern()) { // class关闭了偏向,简单cas即可,可能race codition,但是不要紧
      markOop biased_value       = mark;
      markOop res_mark = obj->cas_set_mark(prototype_header, mark);
      assert(!obj->mark()->has_bias_pattern(), "even if we raced, should still be revoked");
      return BIAS_REVOKED;
    } else if (prototype_header->bias_epoch() != mark->bias_epoch()) { // epoch过时,该对象不是锁
      if (attempt_rebias) {
        assert(THREAD->is_Java_thread(), "");
        markOop biased_value       = mark;
        markOop rebiased_prototype = markOopDesc::encode((JavaThread*) THREAD, mark->age(), prototype_header->bias_epoch());
        markOop res_mark = obj->cas_set_mark(rebiased_prototype, mark); // 尝试重偏向到当前线程
        if (res_mark == biased_value) {
          return BIAS_REVOKED_AND_REBIASED; // 注意这里偏向成功则不会进入slow_enter
        }
      } else {
        markOop biased_value       = mark;
        markOop unbiased_prototype = markOopDesc::prototype()->set_age(mark->age());// 置为无锁
        markOop res_mark = obj->cas_set_mark(unbiased_prototype, mark);
        if (res_mark == biased_value) {
          return BIAS_REVOKED;
        }
      }
    }
  }

如果class关闭了偏向而对象开启偏向,那么说明在其他线程中发生了批量撤销, 直接将mark换成无锁的即可.

如果epoch过时,说明发生了批量重偏向,那么如果允许重偏向,则直接尝试cas掉对象的markword,将threadid换成当前线程的, 如果成功直接返回.
如果不允许重偏向,那么将markword换成无锁的.

如果上述cas中发生了竞争那么,会进入update_heuristics方法中,进行批量重偏向和批量撤销计数器的更新(重偏向阈值低于批量撤销),
根据该类的撤销次数会进行不同的逻辑:

  • 单个撤销
  • 批量重偏向
  • 批量撤销

先看单个撤销,单个撤销的逻辑分为当前该对象偏向的线程是不是当前线程:

php
else if (heuristics == HR_SINGLE_REVOKE) {
    Klass *k = obj->klass();
    markOop prototype_header = k->prototype_header();
    if (mark->biased_locker() == THREAD &&
        prototype_header->bias_epoch() == mark->bias_epoch()) {// 需要撤销当前线程的偏向
      ResourceMark rm;
      log_info(biasedlocking)("Revoking bias by walking my own stack:");
      EventBiasedLockSelfRevocation event;
      BiasedLocking::Condition cond = revoke_bias(obj(), false, false, (JavaThread*) THREAD, NULL);
      ((JavaThread*) THREAD)->set_cached_monitor_info(NULL);
      assert(cond == BIAS_REVOKED, "why not?");
      if (event.should_commit()) {
        post_self_revocation_event(&event, k);
      }
      return cond;
    } else { // 如果不是偏向当前线程需要到safepoint再撤销
      EventBiasedLockRevocation event;
      VM_RevokeBias revoke(&obj, (JavaThread*) THREAD);
      VMThread::execute(&revoke);
      if (event.should_commit() && revoke.status_code() != NOT_BIASED) {
        post_revocation_event(&event, k, &revoke);
      }
      return revoke.status_code();
    }
  }

如上,撤销当前线程的偏向不需要到安全点,因为不可能发生线程安全问题.如果不是当前线程需要push到VMThread中.

批量逻辑如下所示:

php
EventBiasedLockClassRevocation event;
  VM_BulkRevokeBias bulk_revoke(&obj, (JavaThread*) THREAD,
                                (heuristics == HR_BULK_REBIAS),
                                attempt_rebias);
  VMThread::execute(&bulk_revoke);

单个对象偏向撤销

php
uint age = mark->age();
markOop   biased_prototype = markOopDesc::biased_locking_prototype()->set_age(age);
markOop unbiased_prototype = markOopDesc::prototype()->set_age(age);

首先构造了一个匿名偏向的mark和一个无锁的mark.

接着判断当前偏向线程是否存活, 如果不存活那么设置好mark之后立即返回已撤销:

php
bool thread_is_alive = false;
  if (requesting_thread == biased_thread) {
    thread_is_alive = true;
  } else {
    ThreadsListHandle tlh;
    thread_is_alive = tlh.includes(biased_thread);
  }
  if (!thread_is_alive) {
    if (allow_rebias) {
      obj->set_mark(biased_prototype);
    } else {
      obj->set_mark(unbiased_prototype);
    }
    return BiasedLocking::BIAS_REVOKED;

接下来到了撤销逻辑的关键部分, 主要逻辑如下:
遍历对象当前偏向线程的所有lockrecord,查找出class相符合的,将lockrecord的displacedheader设置为null,
接着设置地址最高的lockrecord的displacedheader为该对象无锁的mark,并且对象的mark设置为地址最高的lockrecord的地址:

php
GrowableArray<MonitorInfo*>* cached_monitor_info = get_or_compute_monitor_info(biased_thread);
  BasicLock* highest_lock = NULL;
  for (int i = 0; i < cached_monitor_info->length(); i++) {
    MonitorInfo* mon_info = cached_monitor_info->at(i);
    if (oopDesc::equals(mon_info->owner(), obj)) {
      log_trace(biasedlocking)("   mon_info->owner (" PTR_FORMAT ") == obj (" PTR_FORMAT ")",
                               p2i((void *) mon_info->owner()),
                               p2i((void *) obj));
      // Assume recursive case and fix up highest lock later
      markOop mark = markOopDesc::encode((BasicLock*) NULL);
      highest_lock = mon_info->lock();
      highest_lock->set_displaced_header(mark);
    } else {
      log_trace(biasedlocking)("   mon_info->owner (" PTR_FORMAT ") != obj (" PTR_FORMAT ")",
                               p2i((void *) mon_info->owner()),
                               p2i((void *) obj));
    }
  }
  if (highest_lock != NULL) {
    highest_lock->set_displaced_header(unbiased_prototype);
    obj->release_set_mark(markOopDesc::encode(highest_lock));

批量逻辑

批量重偏向的代码如下:

php
if (bulk_rebias) {
      if (klass->prototype_header()->has_bias_pattern()) {
        int prev_epoch = klass->prototype_header()->bias_epoch();
        klass->set_prototype_header(klass->prototype_header()->incr_bias_epoch());
        int cur_epoch = klass->prototype_header()->bias_epoch();

        // Now walk all threads' stacks and adjust epochs of any biased
        // and locked objects of this data type we encounter
        for (; JavaThread *thr = jtiwh.next(); ) {
          GrowableArray<MonitorInfo*>* cached_monitor_info = get_or_compute_monitor_info(thr);
          for (int i = 0; i < cached_monitor_info->length(); i++) {
            MonitorInfo* mon_info = cached_monitor_info->at(i);
            oop owner = mon_info->owner();
            markOop mark = owner->mark();
            if ((owner->klass() == k_o) && mark->has_bias_pattern()) {
              // We might have encountered this object already in the case of recursive locking
              assert(mark->bias_epoch() == prev_epoch || mark->bias_epoch() == cur_epoch, "error in bias epoch adjustment");
              owner->set_mark(mark->set_bias_epoch(cur_epoch));
            }
          }
        }
      }

      // At this point we're done. All we have to do is potentially
      // adjust the header of the given object to revoke its bias.
      revoke_bias(o, attempt_rebias_of_object && klass->prototype_header()->has_bias_pattern(), true, requesting_thread, NULL);

首先将klass中mark的epoch自增,然后遍历所有java线程中的当前正在使用的该类型的偏向锁对象,将其epoch更新,
从另一方面来说,也就使那些已偏向但偏向线程已退出或dead掉的该类型对象的epoch过期, 这就给了其他线程一个提示,
即可直接替换该对象中mark的threadId来完成重偏向.

批量撤销的逻辑如下:

php
klass->set_prototype_header(markOopDesc::prototype());

    // Now walk all threads' stacks and forcibly revoke the biases of
    // any locked and biased objects of this data type we encounter.
    for (; JavaThread *thr = jtiwh.next(); ) {
    GrowableArray<MonitorInfo*>* cached_monitor_info = get_or_compute_monitor_info(thr);
    for (int i = 0; i < cached_monitor_info->length(); i++) {
        MonitorInfo* mon_info = cached_monitor_info->at(i);
        oop owner = mon_info->owner();
        markOop mark = owner->mark();
        if ((owner->klass() == k_o) && mark->has_bias_pattern()) {
        revoke_bias(owner, false, true, requesting_thread, NULL);
        }
    }
    }

    // Must force the bias of the passed object to be forcibly revoked
    // as well to ensure guarantees to callers
    revoke_bias(o, false, true, requesting_thread, NULL);

将klass中的mark设置为了不可偏向, 然后遍历所有线程栈并且撤销所有该类的偏向锁.

为什么要设置批量重偏向和批量撤销呢?

因为有可能出现一个线程创建了大量对象并且使用同步操作, 之后另一个线程使用这些对象作为锁则会出现大量的撤销操作,
为了防止这种情况, 设置批量重偏向可以给这个类型的对象一个重新使用偏向锁的机会而不是直接膨胀到轻量级锁.

但是当撤销次数继续增长达到批量撤销的阈值时,我们可以认为在该类型的对象上不适合使用偏向锁, 进而把当前所有偏向锁膨胀,而且新建对象也不使用偏向.

重量级锁

以上是膨胀至轻量级锁的逻辑, 接下来我们看如果fast_enter中没有成功重偏向会发生什么(没有开启偏向锁模式也会进入slow_enter):

php
if (UseBiasedLocking) {
    if (!SafepointSynchronize::is_at_safepoint()) {
      BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
        if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
        return;
      }
    } else {
      assert(!attempt_rebias, "can not rebias toward VM thread");
      BiasedLocking::revoke_at_safepoint(obj);
    }
    assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
  }

  slow_enter(obj, lock, THREAD); // 进入slow-enter

slowenter代码如下:

php
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
  markOop mark = obj->mark();
  assert(!mark->has_bias_pattern(), "should not see bias pattern here");

  if (mark->is_neutral()) {// 如果当前无锁,那么尝试使用lockrecord的地址cas掉对象的mark
    lock->set_displaced_header(mark);
    if (mark == obj()->cas_set_mark((markOop) lock, mark)) {
      TEVENT(slow_enter: release stacklock);
      return;
    }
    // cas失败, 膨胀至重量级锁
  } else if (mark->has_locker() &&
             THREAD->is_lock_owned((address)mark->locker())) { // 处理重入
    assert(lock != mark->locker(), "must not re-lock the same lock");
    assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock");
    lock->set_displaced_header(NULL);
    return;
  }

  // 将displaced——header设置为0(unused)
  lock->set_displaced_header(markOopDesc::unused_mark());
  ObjectSynchronizer::inflate(THREAD, // 膨胀
                              obj(),
                              inflate_cause_monitor_enter)->enter(THREAD);
}

接下来进入膨胀重量级锁的逻辑中:

php
for (;;) {
    const markOop mark = object->mark();
    assert(!mark->has_bias_pattern(), "invariant");

    // 当前对象的markword
    // *  Inflated-已膨胀     - 其他线程将其膨胀了,直接返回
    // *  Stack-locked-轻量级锁 - 尝试膨胀
    // *  INFLATING-膨胀中    - 等待其他线程将其膨胀完成
    // *  Neutral-无锁      - 尝试膨胀
    // *  BIASED-偏向状态       - 非法,如果处于偏向锁模式,说明没有撤销,应该先撤销

    // CASE: inflated--已膨胀,直接返回
    if (mark->has_monitor()) {
      ObjectMonitor * inf = mark->monitor();
      assert(inf->header()->is_neutral(), "invariant");
      assert(oopDesc::equals((oop) inf->object(), object), "invariant");
      assert(ObjectSynchronizer::verify_objmon_isinpool(inf), "monitor is invalid");
      return inf;
    }

    // CASE: 正在膨胀,等待自旋然后返回
    if (mark == markOopDesc::INFLATING()) {
      TEVENT(Inflate: spin while INFLATING); // 自旋
      ReadStableMark(object);
      continue;
    }

    // CASE: stack-locked-轻量级锁,尝试设置INFLATING,如果成功替换掉对象的markword
    // 为当前monitor地址

    if (mark->has_locker()) {
      ObjectMonitor * m = omAlloc(Self);
      // 优化:先准备好monitor,然后再设置INFLATING,防止INFLATING时间过长
      m->Recycle();
      m->_Responsible  = NULL;
      m->_recursions   = 0;
      m->_SpinDuration = ObjectMonitor::Knob_SpinLimit;   // Consider: maintain by type/class

      markOop cmp = object->cas_set_mark(markOopDesc::INFLATING(), mark);
      if (cmp != mark) { // 其他线程成功设置, 重试
        omRelease(Self, m, true);
        continue;       // Interference -- just retry
      }

      // 为什么使用一个INFLATING而不是直接设置monitor呢?
      // 这是因为在轻量级锁解锁的过程中,要将displacedHeader还原到对象头中,这时我们设置一个INFLATING
      // 可以让它cas失败,然后进入重量级锁的释放流程,而不是直接还原,造成hashcode值莫名其妙的改变
      markOop dmw = mark->displaced_mark_helper();
      assert(dmw->is_neutral(), "invariant");

      // Setup monitor fields to proper values -- prepare the monitor
      m->set_header(dmw);

      // 将owner设置为lockrecord
      m->set_owner(mark->locker());
      m->set_object(object);

      guarantee(object->mark() == markOopDesc::INFLATING(), "invariant");
      object->release_set_mark(markOopDesc::encode(m)); // 设置对象的mark为monitor的地址

      ...

      return m;
    }

    // CASE: 无锁,尝试直接替换markword

    assert(mark->is_neutral(), "invariant");
    ObjectMonitor * m = omAlloc(Self);
    // prepare m for installation - set monitor to initial state
    m->Recycle();
    m->set_header(mark);
    m->set_owner(NULL);
    m->set_object(object);
    m->_recursions   = 0;
    m->_Responsible  = NULL;
    m->_SpinDuration = ObjectMonitor::Knob_SpinLimit;       // consider: keep metastats by type/class

    if (object->cas_set_mark(markOopDesc::encode(m), mark) != mark) {
      m->set_object(NULL);
      m->set_owner(NULL);
      m->Recycle();
      omRelease(Self, m, true);
      m = NULL;
      continue;
      // interference - the markword changed - just retry.
      // The state-transitions are one-way, so there's no chance of
      // live-lock -- "Inflated" is an absorbing state.
    }

以上就是对应当前对象的锁状态的膨胀逻辑, 具体就是设置了一个objectMonitor对象到对象的markword中.

重量级锁竞争逻辑

代码在objetcMonitor::enter中.

php
void ObjectMonitor::enter(TRAPS) {
  Thread * const Self = THREAD;

  void * cur = Atomic::cmpxchg(Self, &_owner, (void*)NULL);// 尝试cas掉owner字段,只有在owner为空的情况下才成功.
  if (cur == NULL) {
    // Either ASSERT _recursions == 0 or explicitly set _recursions = 0.
    assert(_recursions == 0, "invariant");
    assert(_owner == Self, "invariant");
    return;
  }

  if (cur == Self) { // 重入,简单自增_recursions
    // TODO-FIXME: check for integer overflow!  BUGID 6557169.
    _recursions++;
    return;
  }

  if (Self->is_lock_owned ((address)cur)) { // 当前线程就是轻量级锁的拥有者,直接设置_owner
    assert(_recursions == 0, "internal state error");
    _recursions = 1;
    _owner = Self;
    return;
  }

  Self->_Stalled = intptr_t(this);

  // 尝试自旋
  if (Knob_SpinEarly && TrySpin (Self) > 0) {
    assert(_owner == Self, "invariant");
    assert(_recursions == 0, "invariant");
    assert(((oop)(object()))->mark() == markOopDesc::encode(this), "invariant");
    Self->_Stalled = 0;
    return;
  }

  Atomic::inc(&_count);

  EventJavaMonitorEnter event;
  if (event.should_commit()) {
    event.set_monitorClass(((oop)this->object())->klass());
    event.set_address((uintptr_t)(this->object_addr()));
  }

  { // 设置java线程状态
    JavaThreadBlockedOnMonitorEnterState jtbmes(jt, this);

    Self->set_current_pending_monitor(this);

    OSThreadContendState osts(Self->osthread());
    ThreadBlockInVM tbivm(jt);

    for (;;) {
        // 用于suspend支持,这块没看太懂,但是现在基本不会使用suspend方法了,所以没多大用?
      jt->set_suspend_equivalent();

      EnterI(THREAD); // 进入队列_cxq竞争锁

      if (!ExitSuspendEquivalent(jt)) break;

      // We have acquired the contended monitor, but while we were
      // waiting another thread suspended us. We don't want to enter
      // the monitor while suspended because that would surprise the
      // thread that suspended us.
      //
      _recursions = 0;
      _succ = NULL;
      exit(false, Self);

      jt->java_suspend_self();
    }
    Self->set_current_pending_monitor(NULL);
  }

  Atomic::dec(&_count);
  Self->_Stalled = 0;

  ...
}

可以看到,先进行了一些特殊情况的尝试,然后进入EnterI方法,下面我们来看EnterI方法.

EnterI可分为几个步骤:

  1. 插入cxq队列首部
  2. 作为_Responsible或者普通线程在队列中等待
  3. 获取锁后从cxq或者EntryList中移除(为什么会在EntryList接下来会解释)

_Responsible的park是有限时间的park,它会定期检查monitor的_owner字段,之所以这样设计是防止出现“stranding”即搁浅.

下面是具体代码:

php
void ObjectMonitor::EnterI(TRAPS) {
  Thread * const Self = THREAD;
  assert(Self->is_Java_thread(), "invariant");
  assert(((JavaThread *) Self)->thread_state() == _thread_blocked, "invariant");

  // Try the lock - TATAS
  if (TryLock (Self) > 0) {
    assert(_succ != Self, "invariant");
    assert(_owner == Self, "invariant");
    assert(_Responsible != Self, "invariant");
    return;
  }

  DeferredInitialize();

  if (TrySpin (Self) > 0) {
    assert(_owner == Self, "invariant");
    assert(_succ != Self, "invariant");
    assert(_Responsible != Self, "invariant");
    return;
  }

  ObjectWaiter node(Self);
  Self->_ParkEvent->reset();
  node._prev   = (ObjectWaiter *) 0xBAD;
  node.TState  = ObjectWaiter::TS_CXQ;

  ObjectWaiter * nxt;
  for (;;) { // 插入队首
    node._next = nxt = _cxq;
    if (Atomic::cmpxchg(&node, &_cxq, nxt) == nxt) break;

    // 再次尝试TATAS
    if (TryLock (Self) > 0) {
      assert(_succ != Self, "invariant");
      assert(_owner == Self, "invariant");
      assert(_Responsible != Self, "invariant");
      return;
    }
  }
    // 当EntryList为空时,说明没有正在等待的线程,直接将_Responsible设置为自己
  if ((SyncFlags & 16) == 0 && nxt == NULL && _EntryList == NULL) {
    // Try to assume the role of responsible thread for the monitor.
    // CONSIDER:  ST vs CAS vs { if (Responsible==null) Responsible=Self }
    Atomic::replace_if_null(Self, &_Responsible);
  }

  int nWakeups = 0;
  int recheckInterval = 1;

  for (;;) {

    if (TryLock(Self) > 0) break; // 再次尝试获取TATAS
    assert(_owner != Self, "invariant");

    if ((SyncFlags & 2) && _Responsible == NULL) {
      Atomic::replace_if_null(Self, &_Responsible);
    }

    // 获取锁失败,park自己,分为是不是_Responsible,如果是那么是timedpark,
    // 间隔一定时间后尝试获取_owner
    if (_Responsible == Self || (SyncFlags & 1)) {
      TEVENT(Inflated enter - park TIMED);
      Self->_ParkEvent->park((jlong) recheckInterval);
      // Increase the recheckInterval, but clamp the value.
      recheckInterval *= 8;
      if (recheckInterval > MAX_RECHECK_INTERVAL) {
        recheckInterval = MAX_RECHECK_INTERVAL;
      }
    } else {
      TEVENT(Inflated enter - park UNTIMED);
      Self->_ParkEvent->park();
    }

    if (TryLock(Self) > 0) break;

    ...

    ++nWakeups;

    if ((Knob_SpinAfterFutile & 1) && TrySpin(Self) > 0) break;

    ...

    if (_succ == Self) _succ = NULL; // 释放锁时,succ会被选取
  }


  // 从cxq或者entrylist上unlink自己
  assert(_owner == Self, "invariant");
  assert(object() != NULL, "invariant");
  // I'd like to write:
  //   guarantee (((oop)(object()))->mark() == markOopDesc::encode(this), "invariant") ;
  // but as we're at a safepoint that's not safe.

  UnlinkAfterAcquire(Self, &node);
  if (_succ == Self) _succ = NULL;

  ...

  return;
}

重量级锁的释放

释放逻辑简单来说分为如下几步(非重入释放):
while(true) {
1. 设置owner为null
2. 如果有必要唤醒一个线程,那么唤醒(需要重新获得锁),否则直接退出
3. 将cxq以某种方式拼接到EntryList中
4. 如果此时EntryList不为空那么设置succ和owner字段,唤醒线程返回.否则进入5
5. 再次将cxq插入到EntryList中, 如果EntryList不为空那么唤醒, 否则continue
}

php
for (;;) {
    assert(THREAD == _owner, "invariant");

    if (Knob_ExitPolicy == 0) {// 默认为0
      OrderAccess::release_store(&_owner, (void*)NULL);   // 设置owner为null
      OrderAccess::storeload();                      
      if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != NULL) { // _succ也会在TrySpin中设置,如果存在的话就不用唤醒其他线程了
        TEVENT(Inflated exit - simple egress);
        return;
      }
      TEVENT(Inflated exit - complex egress);
      
      // 由于之后我们要操作cxq和entrylist, 所以需要重新获取锁,如果此处失败那么返回即可,由新的锁拥有者来处理唤醒线程
      if (!Atomic::replace_if_null(THREAD, &_owner)) {
        return;
      }
      TEVENT(Exit - Reacquired);
    } else {
      ...
    }

    ObjectWaiter * w = NULL;
    int QMode = Knob_QMode;

    // Qmode为2,那么直接从cxq中唤醒
    if (QMode == 2 && _cxq != NULL) {
      w = _cxq;
      assert(w != NULL, "invariant");
      assert(w->TState == ObjectWaiter::TS_CXQ, "Invariant");
      ExitEpilog(Self, w);
      return;
    }

    // QMode为3, 那么将cxq 抽干到EntryList中(放在EntryList末尾)
    if (QMode == 3 && _cxq != NULL) {
      w = _cxq;
      for (;;) {
        assert(w != NULL, "Invariant");
        ObjectWaiter * u = Atomic::cmpxchg((ObjectWaiter*)NULL, &_cxq, w); // 将cxq设置为null
        if (u == w) break;
        w = u;
      }
      assert(w != NULL, "invariant");

      ObjectWaiter * q = NULL;
      ObjectWaiter * p;
      for (p = w; p != NULL; p = p->_next) { // 更改状态为TS_ENTER
        guarantee(p->TState == ObjectWaiter::TS_CXQ, "Invariant");
        p->TState = ObjectWaiter::TS_ENTER;
        p->_prev = q;
        q = p;
      }

      // 将cxq放在EntryList末尾
      ObjectWaiter * Tail;
      for (Tail = _EntryList; Tail != NULL && Tail->_next != NULL;
           Tail = Tail->_next)
        /* empty */;
      if (Tail == NULL) {
        _EntryList = w;
      } else {
        Tail->_next = w;
        w->_prev = Tail;
      }

    }

    // / QMode为3, 那么将cxq 抽干到EntryList中(放在EntryList头部)
    if (QMode == 4 && _cxq != NULL) {
      w = _cxq;
      for (;;) {
        assert(w != NULL, "Invariant");
        ObjectWaiter * u = Atomic::cmpxchg((ObjectWaiter*)NULL, &_cxq, w);
        if (u == w) break;
        w = u;
      }
      assert(w != NULL, "invariant");

      ObjectWaiter * q = NULL;
      ObjectWaiter * p;
      for (p = w; p != NULL; p = p->_next) {
        guarantee(p->TState == ObjectWaiter::TS_CXQ, "Invariant");
        p->TState = ObjectWaiter::TS_ENTER;
        p->_prev = q;
        q = p;
      }

      // Prepend the RATs to the EntryList
      if (_EntryList != NULL) {
        q->_next = _EntryList;
        _EntryList->_prev = q;
      }
      _EntryList = w;

      // Fall thru into code that tries to wake a successor from EntryList
    }

    // _EntryList不为空,唤醒_EntryList的头部线程
    w = _EntryList;
    if (w != NULL) {
      assert(w->TState == ObjectWaiter::TS_ENTER, "invariant");
      ExitEpilog(Self, w);
      return;
    }

    // cxq和_EntryList都为空,那么重新将不空的cxq放在_EntryList中
    w = _cxq;
    if (w == NULL) continue;

    for (;;) {
      assert(w != NULL, "Invariant");
      ObjectWaiter * u = Atomic::cmpxchg((ObjectWaiter*)NULL, &_cxq, w);
      if (u == w) break;
      w = u;
    }

    // QMode为1, 将cxq逆序
    if (QMode == 1) {
      // QMode == 1 : drain cxq to EntryList, reversing order
      // We also reverse the order of the list.
      ObjectWaiter * s = NULL;
      ObjectWaiter * t = w;
      ObjectWaiter * u = NULL;
      while (t != NULL) {
        guarantee(t->TState == ObjectWaiter::TS_CXQ, "invariant");
        t->TState = ObjectWaiter::TS_ENTER;
        u = t->_next;
        t->_prev = u;
        t->_next = s;
        s = t;
        t = u;
      }
      _EntryList  = s;
      assert(s != NULL, "invariant");
    } else {
      // QMode == 0 or QMode == 2, 将cxq正序插入, 最晚的线程在最前面
      _EntryList = w;
      ObjectWaiter * q = NULL;
      ObjectWaiter * p;
      for (p = w; p != NULL; p = p->_next) {
        guarantee(p->TState == ObjectWaiter::TS_CXQ, "Invariant");
        p->TState = ObjectWaiter::TS_ENTER;
        p->_prev = q;
        q = p;
      }
    }

    // context-switch rate.
    if (_succ != NULL) continue;

    w = _EntryList;
    if (w != NULL) {
      guarantee(w->TState == ObjectWaiter::TS_ENTER, "invariant");
      ExitEpilog(Self, w);
      return;
    }
  }

ExitEpilog方法很简单,就不解释了:

php
void ObjectMonitor::ExitEpilog(Thread * Self, ObjectWaiter * Wakee) {
  assert(_owner == Self, "invariant");

  // Exit protocol:
  // 1. ST _succ = wakee
  // 2. membar #loadstore|#storestore;
  // 2. ST _owner = NULL
  // 3. unpark(wakee)

  _succ = Knob_SuccEnabled ? Wakee->_thread : NULL;
  ParkEvent * Trigger = Wakee->_event;

  // Hygiene -- once we've set _owner = NULL we can't safely dereference Wakee again.
  // The thread associated with Wakee may have grabbed the lock and "Wakee" may be
  // out-of-scope (non-extant).
  Wakee  = NULL;

  // Drop the lock
  OrderAccess::release_store(&_owner, (void*)NULL);
  OrderAccess::fence();                               // ST _owner vs LD in unpark()

  if (SafepointMechanism::poll(Self)) {
    TEVENT(unpark before SAFEPOINT);
  }

  DTRACE_MONITOR_PROBE(contended__exit, this, object(), Self);
  Trigger->unpark();

  // Maintain stats and report events to JVMTI
  OM_PERFDATA_OP(Parks, inc());
}

关于同步关键字的源码实现分析到此结束, 对于wait/notify等原理的分析会以后再分析吧.

吐槽: 我是个没什么毅力的人, 写这个也只是心血来潮, 而且写的较乱, 一些基本的概念没有介绍,比如markword的不同结构以及ObjectMonitor的组成
,这些可以画一张图的, 这些以后补充吧.

JDK中的AQS和ObjectMonitor设计的有相似之处,但是也有很多不同, 比如这里有Responsible线程, 还有_succ字段, 这些在AQS里面是没有的,
而且JVM的这个monitor只有非公平模式, 而AQS可以是非公平也可以是公平的.还有很多其他的不同这里就不列举了.


File: _posts/2019-07-27-netty-memorypool-concept.md


layout: post title: "Netty内存池原理分析" date: 2019-07-27 02:00:00 +0700 categories: [netty]

Overview

netty基于JEMalloc算法构建了一套高性能的内存分配机制, 下面依据netty源码来介绍一下其原理.

最顶层的是PoolArena, Arena下分为Chunk, Chunk下分为Page, Page又可分为SubPage.
Arena下的Chunk被划分为了几个ChunkList(双向链表):

java
private final PoolChunkList<T> q050;
private final PoolChunkList<T> q025;
private final PoolChunkList<T> q000;
private final PoolChunkList<T> qInit;
private final PoolChunkList<T> q075;
private final PoolChunkList<T> q100;

划分的依据是Chunk的空间使用率, 如下, x轴为使用率:

Chunk的初始状态为INIT.

Chunk是一个较大的内存块, 默认为16MB, 一个Chunk默认分为2048个Page, 每个Page大小为16 * 1024 * 1024 / 2048 = 8192 = 8K,
Page由一个完全平衡二叉树管理, 其中叶子为2048个Page.

如上图所示, 二叉树高12, 叶节点数目为2^11 = 2048, 正好为一个Chunk中Page的数目.
假设我们要在一个尚未开始分配的Chunk中分配一个Tiny类型的空间, 那么它会先判断这个大小会在那一层分配, Tiny小于一个Page的大小所以在
11层分配, 接下来如果分配一个18KB的大小, 该大小需要4个Page(分配空间的规范化, 下面会讲到, 实际会分配32KB), 故需要在第九层分配, 512这个节点无法满足, 那么
选取的是513这个节点进行分配.

每次请求的大小可分为如下三类:

  • Tiny: 16-511
  • Small: 512-8192 (一个Page以内)
  • Normal: 8K-16MB (一个page到一个Chunk)
  • Huge: > 16MB
分类 大小
Tiny 16B32B...480B496B
Small 512B1KB...2KB4KB
Normal 8KB16KB...8MB16MB
Huge 32MB64MB...

请求大小会被规范化, 最小16B, 在到达Small之前以16B递增作为可分配的大小, 然后以2倍递增.

对于Tiny和Small会在一个Page内分配, Netty对Page进一步切分为SubPage, SubPage的大小即第一次分配的请求大小, 同样切分大小的Page会组成一个双向链表
存在Chunk所属的Arena中.


PooledByteBufAllocator

netty中最顶层的类应该是PooledByteBufAllocator, 在它的构造器中会初始化大量内存池参数以及一个PoolArena列表, 如下:

java
threadCache = new PoolThreadLocalCache(useCacheForAllThreads); // 用于在线程本地分配内存
this.tinyCacheSize = tinyCacheSize;
this.smallCacheSize = smallCacheSize;
this.normalCacheSize = normalCacheSize;
chunkSize = validateAndCalculateChunkSize(pageSize, maxOrder);
...
if (nHeapArena > 0) {
    ...
}

if (nDirectArena > 0) {
    directArenas = newArenaArray(nDirectArena);
    List<PoolArenaMetric> metrics = new ArrayList<PoolArenaMetric>(directArenas.length);
    for (int i = 0; i < directArenas.length; i ++) {
        PoolArena.DirectArena arena = new PoolArena.DirectArena(
                this, pageSize, maxOrder, pageShifts, chunkSize, directMemoryCacheAlignment);
        directArenas[i] = arena;
        metrics.add(arena);
    }
    directArenaMetrics = Collections.unmodifiableList(metrics);
} else {
    directArenas = null;
    directArenaMetrics = Collections.emptyList();
}

我们以使用direct内存为例(接下来也是这样)可以看到这里使用相关参数初始化了一个PoolArena.DirectArena数组.

好了, 分析完PooledByteBufAllocator的初始化, 接下来我们看PooledByteBufAllocator的内存分配代码:

java
protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
    PoolThreadCache cache = threadCache.get();
    PoolArena<ByteBuffer> directArena = cache.directArena;

    final ByteBuf buf;
    if (directArena != null) {
        buf = directArena.allocate(cache, initialCapacity, maxCapacity);
    } else {
        buf = PlatformDependent.hasUnsafe() ?
                UnsafeByteBufUtil.newUnsafeDirectByteBuf(this, initialCapacity, maxCapacity) :
                new UnpooledDirectByteBuf(this, initialCapacity, maxCapacity);
    }

    return toLeakAwareBuffer(buf);
}

该方法根据一个初始容量和最大容量分配一个DirectBuffer.
首先判断PoolThreadCache中的directArena是否为空, 这里PoolThreadCache中的directArena是在什么时候设置的呢?

答案是在上面PooledByteBufAllocator的构造函数开始部分的threadCache = new PoolThreadLocalCache(useCacheForAllThreads);,
这里构造了一个PoolThreadLocalCache的对象, 我们看一下该对象的情况, 它继承了FastThreadLocal, 并且重写了initialValue()方法,
从FastThreadLocal的名称我们就可以猜到initialValue()方法是做什么的, 该方法提供一个线程本地变量的初始值,
该初始值是一个PoolThreadCache类型的对象, 我们看一下该方法:

java
protected synchronized PoolThreadCache initialValue() {
    final PoolArena<byte[]> heapArena = leastUsedArena(heapArenas);
    final PoolArena<ByteBuffer> directArena = leastUsedArena(directArenas);

    final Thread current = Thread.currentThread();
    if (useCacheForAllThreads || current instanceof FastThreadLocalThread) {
        final PoolThreadCache cache = new PoolThreadCache(
                heapArena, directArena, tinyCacheSize, smallCacheSize, normalCacheSize,
                DEFAULT_MAX_CACHED_BUFFER_CAPACITY, DEFAULT_CACHE_TRIM_INTERVAL);

        if (DEFAULT_CACHE_TRIM_INTERVAL_MILLIS > 0) {
            final EventExecutor executor = ThreadExecutorMap.currentExecutor();
            if (executor != null) {
                executor.scheduleAtFixedRate(trimTask, DEFAULT_CACHE_TRIM_INTERVAL_MILLIS,
                        DEFAULT_CACHE_TRIM_INTERVAL_MILLIS, TimeUnit.MILLISECONDS);
            }
        }
        return cache;
    }
    // No caching so just use 0 as sizes.
    return new PoolThreadCache(heapArena, directArena, 0, 0, 0, 0, 0);
}

忽略该方法关于直接内存和对内存之间的判断, 我们可以看到首先调用leastUsedArena方法从Arena数组中获取最少被PoolThreadCache使用的Arena.
然后构造一个PoolThreadCache对象, 对于该类的详细原理我们待会再说, 先继续newDirectBuffer的逻辑.

如果我们按照上述流程正常走下去, PoolThreadCache中应该是存在Arena的, 可以到DirectArena的allocate方法内.


PoolArena初步解析

在分析PoolArena的内存分配流程之前, 我们先梳理一下PoolArena的大体结构.
PoolArena的重要属性如下所示:

java
private final int maxOrder; // chunk二叉树的高度,默认11
final int pageSize; // 单个page大小,默认8192
final int pageShifts;  //
final int chunkSize; // chunk大小
final int subpageOverflowMask; // 用于计算是否大于tiny/small
final int numSmallSubpagePools; // small请求的链表头个数
final int directMemoryCacheAlignment; // 对齐内存单位
final int directMemoryCacheAlignmentMask; // 用于对齐内存
private final PoolSubpage<T>[] tinySubpagePools; // 元素为tiny请求的Subpage双向链表头,下标表示subpage的大小,下同
private final PoolSubpage<T>[] smallSubpagePools;// 元素为small请求的Subpage双向链表头
// 各个利用率的chunkList
private final PoolChunkList<T> q050;
private final PoolChunkList<T> q025;
private final PoolChunkList<T> q000;
private final PoolChunkList<T> qInit;
private final PoolChunkList<T> q075;
private final PoolChunkList<T> q100;

各个ChunkList的关系如下所示:

在构造方法中, 除了对上述ChunkList的构造之外, 对一些其他的重要参数也有处理, 如下:

java
this.parent = parent; // PooledByteBufAllocator
this.pageSize = pageSize;
this.maxOrder = maxOrder;
this.pageShifts = pageShifts;
this.chunkSize = chunkSize;
directMemoryCacheAlignment = cacheAlignment;
directMemoryCacheAlignmentMask = cacheAlignment - 1;
subpageOverflowMask = ~(pageSize - 1);
tinySubpagePools = newSubpagePoolArray(numTinySubpagePools);
for (int i = 0; i < tinySubpagePools.length; i ++) {
    tinySubpagePools[i] = newSubpagePoolHead(pageSize);
}

numSmallSubpagePools = pageShifts - 9;// pageShifts默认为13,则此处为4,即512,1k,2k,4k
smallSubpagePools = newSubpagePoolArray(numSmallSubpagePools);
for (int i = 0; i < smallSubpagePools.length; i ++) {
    smallSubpagePools[i] = newSubpagePoolHead(pageSize);
}

需要说明的是tinySubpagePools和smallSubpagePools, 在上面的属性中也可以看到, 这两个数组中的元素都是链表的头,
数组下标的不同表示SubPage的大小, 头是不参与内存的分配的. 在Chunk中对SubPage的初始分配都会将对应的Page加入到对应的链表中.

好的, 分析完了PoolArena的构造方法, 接下来我们可以开始内存分配的分析了, 回到PoolArena的allocate方法中来:

java
PooledByteBuf<T> allocate(PoolThreadCache cache, int reqCapacity, int maxCapacity) {
    PooledByteBuf<T> buf = newByteBuf(maxCapacity);
    allocate(cache, buf, reqCapacity);
    return buf;
}

首先通过newByteBuf方法构造一个未分配内存的PooledByteBuf, 接下来进入重载的allocate方法中:

java
final int normCapacity = normalizeCapacity(reqCapacity); // 规范化请求内存大小
if (isTinyOrSmall(normCapacity)) { // 是否是tiny或者small, 即判断是否小于pageSize
    int tableIdx;
    PoolSubpage<T>[] table;
    boolean tiny = isTiny(normCapacity);
    if (tiny) { // < 512
        if (cache.allocateTiny(this, buf, reqCapacity, normCapacity)) {// 先从线程本地缓存中尝试分配, 如果成功直接返回, 如果没有可供分配的缓存, 那么尝试在Subpage数组中分配, 见如下分析.
            // 如果在线程本地分配成功, 那么直接返回
            return;
        }
        tableIdx = tinyIdx(normCapacity); // tinyIdx方法根据请求大小获得在tinySubpagePools中的数组索引, 即寻找具有合适的Subpage大小的Page链表
        table = tinySubpagePools;
    } else {
        if (cache.allocateSmall(this, buf, reqCapacity, normCapacity)) { // 同上, 只不过分配Small
            return;
        }
        tableIdx = smallIdx(normCapacity);
        table = smallSubpagePools;
    }

    final PoolSubpage<T> head = table[tableIdx]; // 获取链表头节点

    // 在SubpagePool中尝试分配
    // 需要在链表头节点上同步, 因为Chunk中的分配Subpage和free时也会修改链表
    synchronized (head) {
        final PoolSubpage<T> s = head.next;
        if (s != head) { // 如果链表中有除了头节点以外的节点, 即其他Chunk已经分配的Page
            assert s.doNotDestroy && s.elemSize == normCapacity;
            long handle = s.allocate();
            assert handle >= 0;
            s.chunk.initBufWithSubpage(buf, null, handle, reqCapacity);
            incTinySmallAllocation(tiny);
            return;
        }
    }

    // 
    synchronized (this) {
        allocateNormal(buf, reqCapacity, normCapacity);
    }

    incTinySmallAllocation(tiny);
    return;
}// normal大小的请求
if (normCapacity <= chunkSize) {
    if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) {// 尝试在线程本地缓存中分配
        // was able to allocate out of the cache so move on
        return;
    }
    synchronized (this) {
        allocateNormal(buf, reqCapacity, normCapacity);
        ++allocationsNormal;
    }
} else {
    // Huge allocations are never served via the cache so just call allocateHuge
    allocateHuge(buf, reqCapacity); // huge大小的分配不会缓存
}
}

线程本地缓存分配

首先我们看PoolThreadCache的allocateTiny方法.

java
boolean allocateTiny(PoolArena<?> area, PooledByteBuf<?> buf, int reqCapacity, int normCapacity) {
    return allocate(cacheForTiny(area, normCapacity), buf, reqCapacity);
}

首先调用了cacheForTiny方法获取一个MemoryRegionCache, 这个MemoryRegionCache又是什么呢?

MemoryRegionCache

我们首先看MemoryRegionCache在PoolThreadCache中的初始化以及在PoolThreadCache中的作用:

java
private final MemoryRegionCache<byte[]>[] tinySubPageHeapCaches;
private final MemoryRegionCache<byte[]>[] smallSubPageHeapCaches;
private final MemoryRegionCache<ByteBuffer>[] tinySubPageDirectCaches;
private final MemoryRegionCache<ByteBuffer>[] smallSubPageDirectCaches;
private final MemoryRegionCache<byte[]>[] normalHeapCaches;
private final MemoryRegionCache<ByteBuffer>[] normalDirectCaches;

可以看到, 在PoolThreadCache中存在着不同SizeClass的MemoryRegionCache数组, 猜测它应该是缓存的封装.
接着我们看MemoryRegionCache的初始化过程来理解MemoryRegionCache的构造参数的意思.(略去部分代码)

java
tinySubPageDirectCaches = createSubPageCaches(
        tinyCacheSize, PoolArena.numTinySubpagePools, SizeClass.Tiny);
smallSubPageDirectCaches = createSubPageCaches(
        smallCacheSize, directArena.numSmallSubpagePools, SizeClass.Small);

private static <T> MemoryRegionCache<T>[] createSubPageCaches(
        int cacheSize, int numCaches, SizeClass sizeClass) {
        MemoryRegionCache<T>[] cache = new MemoryRegionCache[numCaches];
        for (int i = 0; i < cache.length; i++) {
            cache[i] = new SubPageMemoryRegionCache<T>(cacheSize, sizeClass);
        }
        return cache;
}

可以看到, MemoryRegionCache数组的长度来自Arena中的numTinySubpagePools和numSmallSubpagePools,所以MemoryRegionCache实际上也是和Arena中的SubpagePool对应, 也是根据数组索引来区分Subpage大小不同的Page的.

对于MemoryRegionCache本身来说, 主要由cacheSize和sizeClass来控制, 我们来看MemoryRegionCache本身的构造.

MemoryRegionCache的属性和构造方法如下:

java
private final int size; // 控制内部缓存个数
private final Queue<Entry<T>> queue; // 内部的缓存队列
private final SizeClass sizeClass; 
private int allocations; // 分配次数

MemoryRegionCache(int size, SizeClass sizeClass) {
    this.size = MathUtil.safeFindNextPositivePowerOfTwo(size); // 规范化
    queue = PlatformDependent.newFixedMpscQueue(this.size); // 构造线程安全的Mpsc队列
    this.sizeClass = sizeClass;
}

接着我们看add方法, add方法用于将一个Chunk不用的buffer回收到内部的队列中.

java
public final boolean add(PoolChunk<T> chunk, ByteBuffer nioBuffer, long handle) {
    Entry<T> entry = newEntry(chunk, nioBuffer, handle);
    boolean queued = queue.offer(entry);
    if (!queued) {
        entry.recycle(); // 队列满了, 直接回收完事
    }

    return queued;
}

首先这里构造了一个Entry对象, 用于封装Buffer存储在队列中, Entry详情如下:

java
static final class Entry<T> {
    final Handle<Entry<?>> recyclerHandle;
    PoolChunk<T> chunk;
    ByteBuffer nioBuffer;
    long handle = -1;

    Entry(Handle<Entry<?>> recyclerHandle) {
        this.recyclerHandle = recyclerHandle;
    }

    void recycle() {
        chunk = null;
        nioBuffer = null;
        handle = -1;
        recyclerHandle.recycle(this);
    }
}

再看allocate方法, 它尝试分配一个队列中的缓存

java
public final boolean allocate(PooledByteBuf<T> buf, int reqCapacity) {
    Entry<T> entry = queue.poll();
    if (entry == null) {
        return false;
    }
    initBuf(entry.chunk, entry.nioBuffer, entry.handle, buf, reqCapacity);
    entry.recycle();
    ++ allocations;
    return true;
}

首先尝试从队列中poll, 如果poll返回的是null, 说明当前队列为空, 直接返回false.

否则使用entry中的缓存来初始化传入参数的PooledByteBuf, 接着回收entry更新分配计数器结束.

以上就是MemoryRegionCache的大体作用, 接下来我们回到PoolThreadCache的cacheForTiny方法中来.


如下是cacheForTiny以及其相关调用方法的代码:

java
private MemoryRegionCache<?> cacheForTiny(PoolArena<?> area, int normCapacity) {
    int idx = PoolArena.tinyIdx(normCapacity);
    if (area.isDirect()) {
        return cache(tinySubPageDirectCaches, idx);
    }
    return cache(tinySubPageHeapCaches, idx);
}

/** PoolArena#tinyIdx **/
static int tinyIdx(int normCapacity) {
    return normCapacity >>> 4;
}

/** PoolThreadCache#cache **/
private static <T> MemoryRegionCache<T> cache(MemoryRegionCache<T>[] cache, int idx) {
    if (cache == null || idx > cache.length - 1) {
        return null;
    }
    return cache[idx];
}

首先使用tinyIdx方法对请求大小除以16, 这很好理解, 因为Tiny大小本身小于512, 所以除以16就是为了得出在
MemoryRegionCache数组中的索引, PoolThreadCache#cache方法简单对索引做了范围check直接返回cache[idx]了, 所以其逻辑还是非常简单的.

下面我们进入allocate方法中:

java
private boolean allocate(MemoryRegionCache<?> cache, PooledByteBuf buf, int reqCapacity) {
    boolean allocated = cache.allocate(buf, reqCapacity);
    if (++ allocations >= freeSweepAllocationThreshold) {
        allocations = 0;
        trim();
    }
    return allocated;
}

通过调用MemoryRegionCache的allocate方法(上面已经分析了), 如果在MemoryRegionCache中分配完成的话那么会返回true否则返回false.

好的, 关于线程本地缓存进行分配的分析到此结束, 我们回到Arena的分配流程中去.

SubpagePool分配

我们知道, 在一开始, 线程本地缓存是空的, 因为线程本地缓存的本质是本应回收的内存存放在线程本地变量中用于之后的分配, 所以根据接下来Arena分配的逻辑, 对于Tiny/Small大小的请求来说, 会进入SubpagePool分配的逻辑当中.

首先我们会很想知道SubpagePool中的PoolSubpage是从哪来的, 毕竟在Arena的构造方法中并没有初始化它们, 我们先带着这个问题, 我会在之后解释.

好的,回到Arena的逻辑中来, 如果线程本地缓存分配失败, 接下来:

java
tableIdx = tinyIdx(normCapacity);
table = tinySubpagePools;

// or

tableIdx = smallIdx(normCapacity);
table = smallSubpagePools;

获取对应的索引和上面的MemoryRegionCache数组获取索引的意义相同, 不再赘述.

获取到索引之后, 接着:

java
final PoolSubpage<T> head = table[tableIdx];

synchronized (head) {
    final PoolSubpage<T> s = head.next;
    if (s != head) {
        assert s.doNotDestroy && s.elemSize == normCapacity;
        long handle = s.allocate();
        assert handle >= 0;
        s.chunk.initBufWithSubpage(buf, null, handle, reqCapacity);
        incTinySmallAllocation(tiny);
        return;
    }
}

在head上同步, 接着调用PoolSubpage的allocate方法(注意这个方法没有带参数,比如请求大小什么的,因为Subpage可分配带大小是在一开始就固定的)
对于PoolSubpage的allocate方法的详细实现我们待会再解释, 我们只要知道它返回的handle——一个long值代表着:

  • 高32位: 分配的内存在SubPage中的位置
  • 低32位: Subpage在Chunk中的位置

接着我们使用这个handle初始化一开始传入的PooledByteBuf.(待会解释详细初始化过程) TODO

接着完成之后返回.

ChunkList分配

如果上述分配都不成功的话(一开始的话就会走到这里), 会进入ChunkList分配的阶段, 这里也是一定会完成分配的阶段, 首先我们来看allocateNormal的源码:

java
private void allocateNormal(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
    if (q050.allocate(buf, reqCapacity, normCapacity) || q025.allocate(buf, reqCapacity, normCapacity) ||
        q000.allocate(buf, reqCapacity, normCapacity) || qInit.allocate(buf, reqCapacity, normCapacity) ||
        q075.allocate(buf, reqCapacity, normCapacity)) {
        return;
    }

    // Add a new chunk.
    PoolChunk<T> c = newChunk(pageSize, maxOrder, pageShifts, chunkSize);
    boolean success = c.allocate(buf, reqCapacity, normCapacity);
    assert success;
    qInit.add(c);
}

首先尝试在每个ChunkList中分配, 如果在其中一个ChunkList中分配成功,那么直接返回, 否则创建一个新的Chunk, 在其中分配, 然后将其加入到qInit的
ChunkList中.

ok ,我们来看ChunkList的allocate方法.(我们在这里可以做一个小小的猜测, 在ChunkList的allocate方法中还是会调用Chunk的allocate方法)

java
boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
    if (normCapacity > maxCapacity) {
        return false;
    }

    for (PoolChunk<T> cur = head; cur != null; cur = cur.next) {
        if (cur.allocate(buf, reqCapacity, normCapacity)) {
            if (cur.usage() >= maxUsage) {
                remove(cur);
                nextList.add(cur);
            }
            return true;
        }
    }
    return false;
}

首先判断了请求大小不能超过这个ChunkList中的Chunk可以分配的最大大小, 这个限制值是由这个ChunkList带有的使用率决定的(详见上方的图).

接着遍历链表, 调用Chunk的allocate方法, 如果分配成功那么判断是否需要转移该Chunk.

ok, 归根结底还是调用了Chunk的allocate方法, 下面我们进入Chunk的allocate方法一探究竟. 不过在此之前我们需要先了解Chunk的结构组成.


Chunk

Chunk结构分析

java
final PoolArena<T> arena; // Chunk所在的Arena
final T memory; // 实际的内存,根据是不是direct的有区分,比如direct的是ByteBuffer,heap的是byte[]
final boolean unpooled; // false
final int offset; // 相对memory的偏移(因为要内存对齐,详见PoolArena#offsetCacheLine)
private final byte[] memoryMap; // 记录可分配信息
private final byte[] depthMap; // 存放每个深度信息
private final PoolSubpage<T>[] subpages; // 存放该Chunk的subpage
private final int subpageOverflowMask; // 判断是否大于pageSize的掩码
private final int pageSize; // 默认8K
private final int pageShifts; // 默认13
private final int maxOrder; // 11
private final int chunkSize; // 16MB
private final int log2ChunkSize; // 24
private final int maxSubpageAllocs;// 1 << maxOrder

Chunk的最主要的属性是memoryMap和depthMap.我们知道, Chunk默认被分为2048个Page, 每个Page大小8K, Chunk采用完全二叉树来管理Page, 每个Page作为二叉树的叶节点,
那么二叉树的树高为11, 总共包含2048 * 2 - 1 = 4095个节点.

好的,我们接着来看Chunk的构造方法.

java
PoolChunk(PoolArena<T> arena, T memory, int pageSize, int maxOrder, int pageShifts, int chunkSize, int offset) {
    unpooled = false;
    this.arena = arena;
    this.memory = memory;
    this.pageSize = pageSize;
    this.pageShifts = pageShifts;
    this.maxOrder = maxOrder;
    this.chunkSize = chunkSize;
    this.offset = offset;
    unusable = (byte) (maxOrder + 1); // 12
    log2ChunkSize = log2(chunkSize);
    subpageOverflowMask = ~(pageSize - 1);
    freeBytes = chunkSize;

    maxSubpageAllocs = 1 << maxOrder; // 2^11

    // Generate the memory map.
    memoryMap = new byte[maxSubpageAllocs << 1];// 一共2^12
    depthMap = new byte[memoryMap.length];// 一共2^12
    int memoryMapIndex = 1;
    for (int d = 0; d <= maxOrder; ++ d) {
        int depth = 1 << d;
        for (int p = 0; p < depth; ++ p) {
            memoryMap[memoryMapIndex] = (byte) d;// 全都初始化为对应的深度
            depthMap[memoryMapIndex] = (byte) d;
            memoryMapIndex ++;
        }
    }

    subpages = newSubpageArray(maxSubpageAllocs);
    cachedNioBuffers = new ArrayDeque<ByteBuffer>(8);
}

ok, 对照着构造方法中对memoryMap和depthMap的初始化过程, 我们可以发现它们在一开始都被初始化为对应节点在二叉树中的深度.

depthMap在之后不会改变, 而memoryMap在之后会随着节点的分配而改变, 比如一个叶节点A被分配, 那么这个A节点所在的memoryMap的值
会被修改为unusable即12, 其父节点的memoryMap值会根据其子节点的memoryMap的最小值来决定, 那么A的父节点的memoryMap值应该被更新为11,
其父节点的父节点会被更新为10, 以此类推.

ok, 了解了内存分配对memoryMap的影响, 接下来我们回到allocate的逻辑中来.

allocate分析

java
boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
    final long handle;
    if ((normCapacity & subpageOverflowMask) != 0) { // >= pageSize
        handle =  allocateRun(normCapacity);// 分配多个Page
    } else {
        handle = allocateSubpage(normCapacity);// 在一个Page内分配
    }

    if (handle < 0) {
        return false;
    }
    ByteBuffer nioBuffer = cachedNioBuffers != null ? cachedNioBuffers.pollLast() : null;
    initBuf(buf, nioBuffer, handle, reqCapacity);
    return true;
}

首先判断要分配的大小是否小于一个Page, 如果小于那么直接进入SubPage的逻辑中否则分配多个Page.

我们首先看allocateSubpage.

allocateSubpage

java
private long allocateSubpage(int normCapacity) {
    // 获取对应arena中的Subpage链表中的head
    PoolSubpage<T> head = arena.findSubpagePoolHead(normCapacity);
    int d = maxOrder; // d为要分配的深度, 此处是Subpage,故一定在叶节点, d = 11;
    synchronized (head) {
        int id = allocateNode(d);// 重要逻辑在这
        if (id < 0) {
            return id;
        }

        final PoolSubpage<T>[] subpages = this.subpages;
        final int pageSize = this.pageSize;

        freeBytes -= pageSize;

        int subpageIdx = subpageIdx(id);// 获取subpage在Chunk中的位置
        PoolSubpage<T> subpage = subpages[subpageIdx];
        if (subpage == null) {
            subpage = new PoolSubpage<T>(head, this, id, runOffset(id), pageSize, normCapacity);
            subpages[subpageIdx] = subpage;
        } else {
            subpage.init(head, normCapacity);
        }
        return subpage.allocate();
    }
}
  1. 首先确定要分配的深度, 这里是叶节点, 故d为maxOrder.
  2. 接着调用allocateNode方法,传入参数是深度, 在接下来我们可以看到allocateRun其实也调用了该方法, 只不过深度小于maxOrder.
  3. 获得分配的节点后需要初始化subpage并且添加到arena中 的subpagePool中去.

我们先看allocateNode方法:

java
private int allocateNode(int d) {
    int id = 1;
    int initial = - (1 << d); // 最后d位为0, 其他为1
    byte val = value(id); // 获取memoryMap的值, 这里id=1, 二叉树的root, 从root开始往下
    if (val > d) { // 当memoryMap的值大于某个值说明该节点下面没有足够的空间分配了
        return -1;
    }
    while (val < d || (id & initial) == 0) { // 只要当前id对应的节点还能分配(有余) || id还在d层的上面但节点无余(val==d) -> 继续向下
        id <<= 1; // 继续向下
        val = value(id);
        if (val > d) { // 如果左节点不行,那么尝试右节点(右节点一定可以, 因为已经通过了while条件的判断了)
            id ^= 1;
            val = value(id);
        }
    }
    byte value = value(id);
    assert value == d && (id & initial) == 1 << d : String.format("val = %d, id & initial = %d, d = %d",
            value, id & initial, d);
    setValue(id, unusable); // 将获取到的节点的memoryMap设置为unusable
    updateParentsAlloc(id); // 更新祖先节点
    return id;
}

/** Chunk#value **/
private byte value(int id) {
    return memoryMap[id];
}
/** Chunk#updateParentsAlloc **/
private void updateParentsAlloc(int id) {
    while (id > 1) { // 没到达root, root没有父节点
        int parentId = id >>> 1; // 父节点
        byte val1 = value(id);
        byte val2 = value(id ^ 1);
        byte val = val1 < val2 ? val1 : val2; // 获取自己和兄弟节点的value, 选取小的作为parent的value
        setValue(parentId, val);
        id = parentId;
    }
}

首先我们假设d = 11, 即分配的是叶节点来解释allocate的代码, 这段代码实现了伙伴算法的分配思想.

  1. 判断当前root是否可以分配, 这里会有同学想问val = d可不可以,当然不可以,因为这相当于要分配一个Page, er这个大小是不能通过(normCapacity & subpageOverflowMask) != 0这个检查的.
  2. 进入while循环, 这个循环的作用主要是找到能够满足当前需要的最低级节点, 而且是从左往右分配. 然后我们再来解读这个while循环的条件:
    首先判断val < d, 这个式子成立的意思是当前节点的两个节点都满足分配; 第二个式子是(id & initial) == 0, initial只有最后11位是0, 其他全是1, 这意味着想要这个条件成立,必须id小于2^11,这就意味着id不能是目标深度节点才可以进入循环, 因为进入循环就意味着要向下一层前进, 而且第二个式子的隐含条件是val==d,即只有右孩子节点是正好满足要求的.
  3. 进入循环, 首先id左移, 相当于深度加一, 如果左孩子无法分配, 那么与1异或(左孩子永远都是偶数), 得到右孩子, 继续循环.
  4. 跳出循环, 这里id就是我们要找的节点, 我们将其设置为12,并且更新节点的memoryMap的值知道父节点(如前所述).返回id, id即节点在二叉树中的索引.

ok, 分析完了allocateNode方法, 我们继续回到allocateSubpage方法中

java
int id = allocateNode(d);// 我们这里返回了在二叉树中的索引
if (id < 0) {
    return id;
}

final PoolSubpage<T>[] subpages = this.subpages;
final int pageSize = this.pageSize;

freeBytes -= pageSize;

int subpageIdx = subpageIdx(id);// 获取subpage在Chunk中的位置
PoolSubpage<T> subpage = subpages[subpageIdx];
if (subpage == null) {
    subpage = new PoolSubpage<T>(head, this, id, runOffset(id), pageSize, normCapacity); // runOffset(id)获取的是在Chunk中的内存偏移量
    subpages[subpageIdx] = subpage;
} else {
    subpage.init(head, normCapacity);
}
return subpage.allocate();

/** subpageIdx **/
private int subpageIdx(int memoryMapIdx) {
    return memoryMapIdx ^ maxSubpageAllocs; // remove highest set bit, to get offset
}

我们获取了分配节点在Chunk二叉树中的索引, 接着调用subpageIdx方法获取其在subpages中的偏移量(因为叶节点的id是大于subpages的长度即1 << 11 的, 所以此处将最高位的1移除).

获取到subpages数组中对应位置的PoolSubpage后根据其是否已经分配进行后续的逻辑:

  • 未分配: 初始化一个PoolSubpage,放到Chunk的subpages中.
  • 已分配: 说明以前分配过但现在free了, 重新init即可.

我们分别来看这两种情况之前需要分析PoolSubpage的组成.

PoolSubpage结构分析

java
final PoolChunk<T> chunk; // 所属chunk
private final int memoryMapIdx; // memoryMap数组的索引
private final int runOffset; // 在chunk中的内存偏移量
private final int pageSize; 
private final long[] bitmap; // 表示Subpage的分配信息,长度为pageSize / 16 / 64,64代表是long,16代表每个位16B(对应最小16B), 默认为8

PoolSubpage(PoolSubpage<T> head, PoolChunk<T> chunk, int memoryMapIdx, int runOffset, int pageSize, int elemSize) {
    this.chunk = chunk;
    this.memoryMapIdx = memoryMapIdx;
    this.runOffset = runOffset;
    this.pageSize = pageSize;
    bitmap = new long[pageSize >>> 10]; // pageSize / 16 / 64
    init(head, elemSize);
}

void init(PoolSubpage<T> head, int elemSize) {
    doNotDestroy = true;
    this.elemSize = elemSize;
    if (elemSize != 0) {
        maxNumElems = numAvail = pageSize / elemSize; // 可分配个数
        nextAvail = 0;
        bitmapLength = maxNumElems >>> 6; // 可分配个数除以64, 计算bitmap的长度
        if ((maxNumElems & 63) != 0) { // 此处补上一步整除的余数, 假设maxNumElems = 100, 那么上一步得出的结果为1, 此处再加一
            bitmapLength ++;
        }

        for (int i = 0; i < bitmapLength; i ++) {// 初始化bitmap
            bitmap[i] = 0;
        }
    }
    addToPool(head);
}

private void addToPool(PoolSubpage<T> head) { // 简单插入head的next中
    assert prev == null && next == null;
    prev = head;
    next = head.next;
    next.prev = this;
    head.next = this;
}

init方法参数head为Arena中Subpage链表的head, elemSize为Subpage的分配大小.

如上所示, PoolSubpage的初始化过程主要是bitmap数组的初始化, bitmap主要表示一个Page中的分配情况,
bitmap是long数组, 每个long中的每一位表示一次分配.每一次分配都会在bitmap中的long中的一位置1, 在每一个long中从右向左分配.

例如, 假设该Page的可分配大小为1K, 那么可分配个数为8, 所以一个long就可以表示完成表示.

假设分配大小为32, 那么可分配个数就为256, 需要long的个数为4, 所以bitmap的长度为4.

ok, 分析完了subpage的大致结构, 我们再来看它的allocate方法.

java
long allocate() {
    if (elemSize == 0) {
        return toHandle(0);
    }

    if (numAvail == 0 || !doNotDestroy) { // 分配大小过大或者doNotDestroy为false,返回分配失败
        return -1;
    }

    final int bitmapIdx = getNextAvail(); // 获取可用的bitmap索引,第一次分配时为0
    int q = bitmapIdx >>> 6; // 第几个long?
    int r = bitmapIdx & 63; // long中的第几个?
    assert (bitmap[q] >>> r & 1) == 0; // 一定没分配
    bitmap[q] |= 1L << r; // 把对应的位置为1

    if (-- numAvail == 0) {
        removeFromPool(); // 从链表中unlink
    }
    // 0x4000000000000000L | (long) bitmapIdx << 32 | memoryMapIdx;
    return toHandle(bitmapIdx);  // 即前32位放bitmap, 后32位放memoryMap的索引即Page在Chunk中的位置
}
/**---------------------------**/
private int getNextAvail() {
    int nextAvail = this.nextAvail;
    if (nextAvail >= 0) { // 初始时为0
        this.nextAvail = -1;
        return nextAvail;
    }
    return findNextAvail(); // 如果不是第一次分配
}
/**---------------------------**/
private int findNextAvail() {
    final long[] bitmap = this.bitmap;
    final int bitmapLength = this.bitmapLength;
    for (int i = 0; i < bitmapLength; i ++) { // 遍历bitmap,寻找没有被占用的空间
        long bits = bitmap[i];
        if (~bits != 0) { // bits的反不为0说明bits存在0, 即Subpage没有分配的位置
            return findNextAvail0(i, bits); // i为当前第几个long, bits为当前long
        }
    }
    return -1;
}
/**---------------------------**/
private int findNextAvail0(int i, long bits) {
    final int maxNumElems = this.maxNumElems;
    final int baseVal = i << 6; 

    for (int j = 0; j < 64; j ++) {// 遍历bits的每一位, 寻找0, 在分配时1是从右向左分配的
        if ((bits & 1) == 0) { // 找到0了
            int val = baseVal | j; 
            if (val < maxNumElems) {
                return val;
            } else {
                break;
            }
        }
        bits >>>= 1; // 这一位是1,继续向左寻找
    }
    return -1;
}
  1. 首先获取下一个可用的bitmapIdx,.(bitmapIdx = 64 * (bitmap数组索引) + long中第某位比如bitmapIdx = 106, 那么就意味着在bitmap中的第2个long中的第42位(从右往左) )
    1.1 nextAvail不为负数时,说明已经计算好了(可能时第一次分配)此时直接返回即可,否则进入1.2
    1.2 遍历bitmap, 找到bitmap不全为1的long, 然后在这个long中找到位为0的位置, 即findNextAvail0中, 找到之后,返回baseVal | j, baseVal为当前long的起始idx, j为0在当前long的位置, 故此处直接或就可以得到0在整个bitmap中的位置.

  2. 找到了0之后,将此处的0置为1,即bitmap[q] |= 1L << r.

  3. 如果该Page经过这次分配无法再分配了, 那么从subpagePool中移除.

  4. 返回一个handle的long值, 前32位表示在Subpage中的位置, 后32位表示Page在Chunk中的位置.

ok,此处返回了handle之后, 我们再回过头看Chunk的逻辑:

java
boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
    final long handle;
    if ((normCapacity & subpageOverflowMask) != 0) { // >= pageSize
        handle =  allocateRun(normCapacity);
    } else {
        handle = allocateSubpage(normCapacity); // 我们刚刚分析的是这个方法
    }

    if (handle < 0) {
        return false;
    }
    ByteBuffer nioBuffer = cachedNioBuffers != null ? cachedNioBuffers.pollLast() : null;
    initBuf(buf, nioBuffer, handle, reqCapacity); 
    return true;
}

我们刚刚分析的是allocateSubpage的一系列方法, 接下来我们看allocateRun方法, allocateRun方法比较简单, 其中allocateNode
方法我们已经分析过了, 就不再赘述了,我们快速过一遍即可.

java
private long allocateRun(int normCapacity) {
    int d = maxOrder - (log2(normCapacity) - pageShifts); // 找到第几层
    int id = allocateNode(d); // 在该层上分配, 获取id, id为memoryMap的索引
    if (id < 0) {
        return id;
    }
    freeBytes -= runLength(id); // freeBytes减去id节点分配的大小
    return id;
}

private int runLength(int id) {
    return 1 << log2ChunkSize - depth(id); // id代表的节点有多大
}

ok, 家下来我们进入initBuf(buf, nioBuffer, handle, reqCapacity); 方法, 分析PooledByteBuffer是如何根据分配到的handle初始化的.

PooledByteBuf在Chunk中的初始化

java
void initBuf(PooledByteBuf<T> buf, ByteBuffer nioBuffer, long handle, int reqCapacity) {
    int memoryMapIdx = memoryMapIdx(handle);// 获取handle的低32位
    int bitmapIdx = bitmapIdx(handle);// 获取高32位
    if (bitmapIdx == 0) {
        byte val = value(memoryMapIdx); // memoryMap[memoryMapIdx]
        assert val == unusable : String.valueOf(val);
        buf.init(this, nioBuffer, handle, runOffset(memoryMapIdx) + offset,
                reqCapacity, runLength(memoryMapIdx), arena.parent.threadCache());
    } else {
      int memoryMapIdx = memoryMapIdx(handle);
        PoolSubpage<T> subpage = subpages[subpageIdx(memoryMapIdx)];
        buf.init(
            this, nioBuffer, handle,
            runOffset(memoryMapIdx) + (bitmapIdx & 0x3FFFFFFF) * subpage.elemSize + offset,
                reqCapacity, subpage.elemSize, arena.parent.threadCache());
    }
}

private int runOffset(int id) {
    // represents the 0-based offset in #bytes from start of the byte-array chunk
    int shift = id ^ 1 << depth(id);
    return shift * runLength(id);
}

private int runLength(int id) {
    // represents the size in #bytes supported by node 'id' in the tree
    return 1 << log2ChunkSize - depth(id);
}

我们来分析一下initBuf方法.
根据bitmapIdx是否为0分为两个分支, bitmapIdx如果为0说明是第一次在Page上分配或者是normal分配, 否则则是Page已经有分配, 所以要计算一个位移.
我们可以对比两个分支的buf.init方法的参数的不同.

  1. 第一次分配或normal分配:runOffset(memoryMapIdx) + offset,runOffset根据memoryId计算二叉树节点在Chunk中的位移, offset则是
    Chunk中实际开始分配的内存相对于memory的位移(详见PoolArena#offsetCacheLine), 两者加在一起则是此次分配在Chunk中的位移.
    第二次分配则是:unOffset(memoryMapIdx) + (bitmapIdx & 0x3FFFFFFF) * subpage.elemSize + offset, 比第一次多了个
    (bitmapIdx & 0x3FFFFFFF) * subpage.elemSize, Chunk的最大大小是1<<30(见PooledByteBufAllocator#MAX_CHUNK_SIZE), 所以bitmapIdx一定在1<<30之内, 而0x3FFFFFFF为2^30 - 1.这个多出来的位移正是Subpage上其他分配的内存所占用的.
  2. 第一次分配或normal分配:runLength(memoryMapIdx), 这个参数的意思是分配的最大大小, 和reqCapacity相对照.subpage.elemSize也是同样.

最终会调用到pooledBytebuf的init0方法:

java
private void init0(PoolChunk<T> chunk, ByteBuffer nioBuffer,
                    long handle, int offset, int length, int maxLength, PoolThreadCache cache) {
    assert handle >= 0;
    assert chunk != null;

    this.chunk = chunk;
    memory = chunk.memory;
    tmpNioBuf = nioBuffer;
    allocator = chunk.arena.parent;
    this.cache = cache;
    this.handle = handle;
    this.offset = offset;
    this.length = length;
    this.maxLength = maxLength;
}

ok, 仅仅是设置了一些属性值, PooledByteBuf及其子类的实现都比较简单, 在此不再赘述.

ok以上就是netty内存池内存分配相关的代码解读, 接下来我们开始解读另一半: 内存的回收与释放.
我们按照: Subpage -> Chunk -> ChunkList -> Arena的顺序解读内存的回收.

内存的回收

Subpage内存的释放

java
boolean free(PoolSubpage<T> head, int bitmapIdx) {
    if (elemSize == 0) {
        return true;
    }
    int q = bitmapIdx >>> 6;
    int r = bitmapIdx & 63;
    assert (bitmap[q] >>> r & 1) != 0;
    bitmap[q] ^= 1L << r; // 将bitmap中的位置为0

    setNextAvail(bitmapIdx); // 设置当前bitmapIdx为nextAvail

    if (numAvail ++ == 0) {// 又可以用了, 加入到SubpagePool中
        addToPool(head);
        return true;
    }

    if (numAvail != maxNumElems) { // Subpage还存在被分配的
        return true;
    } else {
        if (prev == next) { // 链表至少要保证一个存在
            return true;
        }

        doNotDestroy = false; // 可以释放了
        removeFromPool();
        return false;
    }
}
  1. 首先将bitmap还原
  2. 设置nextAvail
  3. 如果必要,将当前Subpage加入到SubpagePool中去
  4. 根据Subpage是否还在使用返回true or false

Chunk内存的释放

java
void free(long handle, ByteBuffer nioBuffer) {
    int memoryMapIdx = memoryMapIdx(handle);
    int bitmapIdx = bitmapIdx(handle);

    if (bitmapIdx != 0) { // 释放Subpage
        PoolSubpage<T> subpage = subpages[subpageIdx(memoryMapIdx)];
        assert subpage != null && subpage.doNotDestroy;

        PoolSubpage<T> head = arena.findSubpagePoolHead(subpage.elemSize);
        synchronized (head) {
            if (subpage.free(head, bitmapIdx & 0x3FFFFFFF)) { // Subpage还在使用直接返回否则释放Subpage并且从pool中移除
                return;
            }
        }
    }
    freeBytes += runLength(memoryMapIdx);
    setValue(memoryMapIdx, depth(memoryMapIdx)); // 还原memoryMap节点值
    updateParentsFree(memoryMapIdx); // 更新祖先节点memoryMap

    if (nioBuffer != null && cachedNioBuffers != null &&
            cachedNioBuffers.size() < PooledByteBufAllocator.DEFAULT_MAX_CACHED_BYTEBUFFERS_PER_CHUNK) {
        cachedNioBuffers.offer(nioBuffer);
    }
}

释放Subpage的时候需注意是否是链表的最后一个, 如果是那么即使完全没有分配也继续留在链表里,否则从链表中移除
并且还原memoryMap, 这样就可以继续在Chunk中分配给其他请求.

为什么这里链表只需要一个就够了呢, 因为多个并不能带来什么增益反而会因为SUbpage个数爆炸得不到释放造成Chunk的可分配内存下降, 而保证只有一个
可以满足Arena分配内存的fast-path(避免重新在Chunk中分配).

ChunkList中内存的释放

java
boolean free(PoolChunk<T> chunk, long handle, ByteBuffer nioBuffer) {
    chunk.free(handle, nioBuffer);
    if (chunk.usage() < minUsage) {
        remove(chunk);
        return move0(chunk);
    }
    return true;
}

private boolean move0(PoolChunk<T> chunk) {
    if (prevList == null) {
        // There is no previous PoolChunkList so return false which result in having the PoolChunk destroyed and
        // all memory associated with the PoolChunk will be released.
        assert chunk.usage() == 0;
        return false;
    }
    return prevList.move(chunk);
}

private boolean move(PoolChunk<T> chunk) {
    assert chunk.usage() < maxUsage;

    if (chunk.usage() < minUsage) {
        // Move the PoolChunk down the PoolChunkList linked-list.
        return move0(chunk);
    }

    // PoolChunk fits into this PoolChunkList, adding it here.
    add0(chunk);
    return true;
}

调用上述的Chunk释放内存的方法, 主要的逻辑是根据释放后Chunk的内存使用情况,将其下移至其他ChunkList.
该方法的返回值:true代表Chunk不可释放,false代表chunk.usage() == 0

Arena中内存的释放

java
void freeChunk(PoolChunk<T> chunk, long handle, SizeClass sizeClass, ByteBuffer nioBuffer, boolean finalizer) {
    final boolean destroyChunk;
    ...
    if (destroyChunk) {
        // 调用unsafe相关方法释放Chunk内存
        destroyChunk(chunk);
    }
}

free单个PooledByteBuf

java
void free(PoolChunk<T> chunk, ByteBuffer nioBuffer, long handle, int normCapacity, PoolThreadCache cache) {
    if (chunk.unpooled) {
        ...
    } else {
        SizeClass sizeClass = sizeClass(normCapacity);
        if (cache != null && cache.add(this, chunk, nioBuffer, handle, normCapacity, sizeClass)) { 
          // 调用线程本地缓存.将对应Chunk中handle对应的内存缓存起来不释放
            return;
        }

        freeChunk(chunk, handle, sizeClass, nioBuffer, false);
    }
}

/** PoolThreadCache#add **/
boolean add(PoolArena<?> area, PoolChunk chunk, ByteBuffer nioBuffer,
            long handle, int normCapacity, SizeClass sizeClass) {
    MemoryRegionCache<?> cache = cache(area, normCapacity, sizeClass);
    if (cache == null) {
        return false;
    }
    return cache.add(chunk, nioBuffer, handle);
}

总结

以上就是Netty的内存分配以及回收代码的解读,
关于其中代码的细节部分可能会有很多解读不到位的地方, 欢迎各位在我的邮箱留言或者在github上提issue.


File: _posts/2019-07-29-netty-threadmodel-analysis.md


layout: post title: "Netty线程模型分析" date: 2019-08-03 21:50:00 +0700 categories: [netty]

概述

本文通过Netty源码分析Netty的线程模型.

我们先来概述每个类和接口的作用:

  1. EventExecutorGroup: 负责通过next方法提供EventExecutor, 除此之外, 还提供了一系列方法用于管理EventExecutor
    的生命周期.

  2. EventExecutor: 一种特殊的EventExecutorGroup, 它的next方法返回它自己. 它提供了inEventLoop方法判断一个线程是否在eventLoop中.

  3. EventLoopGroup: EventExecutor的子接口, 可以注册一个Channel到EventLoopGroup上, 并且可通过next方法产生EventLoop.

  4. OrderedEventExecutor: 标记接口, 表示使用顺序执行处理提交的task(EventExecutorsubmit提交的task)

  5. EventLoop: 提供parent方法用于返回所属的EventLoopGroup.

  6. AbstractEventExecutor: 对ExecutorService的基本实现, 不支持定时调度功能(即:ScheduledExecutorService接口功能)

  7. AbstractScheduledEventExecutor: 继承自AbstractEventExecutor, 增加定时调度功能, 内含一个优先级队列.

  8. SingleThreadEventExecutor: 继承自AbstractScheduledEventExecutor, 内部含有一个task队列, 用于存放外部执行请求.

  9. SingleThreadEventLoop: 继承自SingleThreadEventExecutor, 实现了EventLoop接口, 增加了注册channel的功能.

  10. NioEventLoop: 继承自SingleThreadEventLoop, 实现了SingleThreadEventExecutor的run方法, 内含一个事件循环, 使用NIO selector执行IO任务和非IO任务.

  11. AbstractEventExecutorGroup: 使用next方法委托给子EventExecutor执行相关方法.

  12. MultithreadEventExecutorGroup: 主要含有EventExecutorChooser, 是next方法具体执行的策略

  13. MultithreadEventLoopGroup: 主要增加注册Channel的功能

  14. NioEventLoopGroup: 实现newChild方法, 构造子EventLoop, 即NioEventLoop.

Reactor模式简介

下面解释一下这张图片里几个组件的作用.

  1. Handle: 可以是任意的资源, Selector在这些资源上等待事件发生. 例如一个电典型的客户端/服务端程序, 一个服务端保持着多个客户端的连接,
    这种连接即Handle, Selector在这些连接上等待读写等事件的发生.

  2. Selecotr: 经典的Reacotr模式中, 它叫做Synchronous Event Demultiplexer(同步事件分离器). 它在一个handle_set上等待事件的发生.

  3. Initiation Dispatcher: 定义了一系列管理Event Handler的接口, 包括注册, 注销Event Handler.一般来说, Selector在返回后会通知Initiation Dispatcher
    ,然后Initiation Dispatcher回调应用指定的Handler.

  4. Event Handler: 定义了用于事件发生时的回调方法, 来处理这些事件.

Reactor模式流程

在此我们将以最经典的Reactor模型来阐述Reactor模式的工作流程.

  1. MainReactor: 对应于Initiation Dispatcher, 它只有一个Handler, 即Acceptor作为注册的Event Handler.在MainReactor包含一个Event Loop, 该Loop调用select方法, , 当有新的连接请求到来, 调用Acceptor的回调方法进行处理.
  2. Acceptor: 初始化时, 注册到MainReactor中, 并且指定监听的端口(监听的端口即Handle). Acceptor处理连接事件的逻辑是: 将新的连接注册到SubReacotr上, 并且指定监听事件类型是可读/可写等, 这样该连接的后续事件都将由SubReactor进行处理.
  3. SubReactor: 负责分配读写事件给注册到该SubReactor的Handler, 一个SubReactor可同时管理多个连接(Handle).
  4. ThreadPool: 负责处理非IO任务.

Netty的线程模型

如上所示:
BossEventLoopGroup相当于MainReactor, 它用来处理新的连接请求, 一旦有新的连接请求到来, BossEventLoopGroup相当于MainReactor会将这个请求分配给ServerBootstrapAcceptor, ServerBootstrapAcceptor将会把这个新的连接注册到ChildEventLoopGroup中的一个EventLoop中, 后续的读写事件都由这个EventLoop进行处理.

关键源码分析

一. `ServerBootstrapAcceptor`作用分析

ServerBootstrap#bind(int inetPort)
-doBind(localAddress)
-initAndRegister()
-ServerBootstrap#init(channel)

如下是ServerBootstrap#init(channel)中关于Acceptor创建的代码:

java
void init(Channel channel) throws Exception {
    // 省略部分代码
    ... 

    p.addLast(new ChannelInitializer<Channel>() {
    @Override
    public void initChannel(final Channel ch) throws Exception {
        final ChannelPipeline pipeline = ch.pipeline();
        ChannelHandler handler = config.handler();
        if (handler != null) {
            pipeline.addLast(handler);
        }

        ch.eventLoop().execute(new Runnable() {
            @Override
            public void run() {
                pipeline.addLast(new ServerBootstrapAcceptor(
                        ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
            }
        });
    }
});
}

init方法参数的channel即即将注册到BossEventLoop中到channel, 负责监听本地端口, 此时在init方法内还在进行channel以及channel对应到pipeline到构造.此处即为channel的pipeline添加了一个ServerBootstrapAcceptor.

ServerBootstrapAcceptor是ChannelInboundHandlerAdapter的子类, 即它是处理入站事件的处理器.

我们依次解释一下构造ServerBootstrapAcceptor所需的参数:

  1. ch: 即ServerSocketChannel
  2. currentChildGroup: ServerBootstrap中的currentChildGroup, 在构造ServerBootstrap中的currentChildGroup时传入
  3. currentChildOptions: 子Channel的Option
  4. currentChildAttrs: 子Channel的AttributeKey

下面查看ServerBootstrapAcceptor的源码, 可以想到ServerBootstrapAcceptor既然是Acceptor那么它一定有将新的连接请求注册到childEventLoopGroup中的逻辑.

java
// ServerBootstrapAcceptor#channelRead
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    final Channel child = (Channel) msg;
    // childHandler
    child.pipeline().addLast(childHandler);

    setChannelOptions(child, childOptions, logger);

    for (Entry<AttributeKey<?>, Object> e: childAttrs) {
        child.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
    }

    try {
        childGroup.register(child).addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                if (!future.isSuccess()) {
                    forceClose(child, future.cause());
                }
            }
        });
    } catch (Throwable t) {
        forceClose(child, t);
    }
}

处理逻辑如下:

  1. 将msg强制转型为Channel(关于这里为什么可以正常转型我们在其他文章中详述), 然后添加子Channel对应的Handler(很可能是我们配置bootstrap时的ChannelInitializer)
  2. 设置Channel的Option和Attr
  3. 最关键到来了: 将子Channel注册到childGroup上.

二. ChildGroup的注册Channel流程

MultithreadEventLoopGroup#register(Channel channel)
->SingleThreadEventLoop#register(Channel channel)
->register(final ChannelPromise promise)
->Unsafe#register(EventLoop eventLoop, final ChannelPromise promise)
->register0(ChannelPromise promise)
->AbstractNioUnsafe#doRegister()

java
// MultithreadEventLoopGroup#register
@Override
public ChannelFuture register(Channel channel) {
    return next().register(channel);
}

next方法以近似于一种轮训的方式获取group中的下一个EventLoop, 进行实际的注册.

java
public ChannelFuture register(Channel channel) {
    return register(new DefaultChannelPromise(channel, this));
}

public ChannelFuture register(final ChannelPromise promise) {
    promise.channel().unsafe().register(this, promise);
    return promise;
}

可以看到最终还是调用了Unsafe类的register方法.

java
private void register0(ChannelPromise promise) {
    try {
        if (!promise.setUncancellable() || !ensureOpen(promise)) {
            return;
        }
        boolean firstRegistration = neverRegistered;
        doRegister();
        neverRegistered = false;
        registered = true;

        pipeline.invokeHandlerAddedIfNeeded();

        safeSetSuccess(promise);
        pipeline.fireChannelRegistered();
        if (isActive()) {
            if (firstRegistration) {
                pipeline.fireChannelActive();
            } else if (config().isAutoRead()) {
                beginRead();
            }
        }
    } catch (Throwable t) {
        closeForcibly();
        closeFuture.setClosed();
        safeSetFailure(promise, t);
    }
}

上述代码的逻辑如下:

  1. 首先确保Channel没有close
  2. 调用doRegister
  3. 调用pipeline.invokeHandlerAddedIfNeeded(), 因为在channel注册完成之前可能有handler加入到pipeline中, 我们需要等待注册完毕才触发HandlerAddedI.
  4. 只有第一次注册时才触发channel active事件.

下面我我们看doRegister的逻辑, doRegister在AbstractUnsafe中是一个空实现, 我们看AbstractNioUnsafe的实现如下:

java
protected void doRegister() throws Exception {
    boolean selected = false;
    for (;;) {
        try {
            selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
            return;
        } catch (CancelledKeyException e) {
            if (!selected) {
                eventLoop().selectNow();
                selected = true;
            } else {
                throw e;
            }
        }
    }
}

实际上就是调用了javaChannel的register方法.
注意这里的异常处理, 这里catch了CancelledKeyException, 这里有可能发生该异常的原因是该channel已经注册在selector上了,但是当前channle对应的selectionKey已经取消了, 如果再次调用register方法的话会造成此异常.
所以此处再次selectNow, 因为selectionKey的cancel方法会将当前key放入selector的cancel-set中,只有再一次select才会从selector中移除.


File: _posts/2019-09-07-rocketmq-broker-message-handle.md


layout: post title: "Rocketmq broker消息存储流程" date: 2019-09-08 02:06:00 +0700 categories: [rocketmq]

源码阅读目标

搞清楚rocketmq如何将接收的消息根据不同的请求命令进行不同的处理, 和消息存储的流程.

前置准备

Netty服务端

在Broker启动过程中,会启动一个Netty服务用来接收producer或者consumer发来的请求, 对请求进行解码, 然后根据不同的RequestCommandCode将
不同的请求转发到不同的Processor中.
具体代码如下:

java
ServerBootstrap childHandler =
    this.serverBootstrap.group(this.eventLoopGroupBoss, this.eventLoopGroupSelector)
        // ...省略TCP socket配置
        .childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            public void initChannel(SocketChannel ch) throws Exception {
                ch.pipeline()
                    .addLast(defaultEventExecutorGroup, HANDSHAKE_HANDLER_NAME, handshakeHandler)
                    .addLast(defaultEventExecutorGroup,
                        encoder,
                        new NettyDecoder(), // 注意这里是请求解码器
                        new IdleStateHandler(0, 0, nettyServerConfig.getServerChannelMaxIdleTimeSeconds()),
                        connectionManageHandler,
                        serverHandler
                    );
            }
        });

如上, rocketmq注册了几个handler, 我们主要关注解码, 下面对其他的ChannelHandler简要解释:

  1. encoder, 编码器,用于将返回消息进行编码,默认使用Json.
  2. decoder, 解码器, 下面会对其进行解释.
  3. IdleStateHandler, Netty内置的处理器, 第一个参数指明Channel多长时间没有数据读入后触发一个READER_IDLE事件, 第二个参数.
    指明写操作空闲的时间触发WRITER_IDLE, 第三个参数控制读写操作均未发生的时间到达时, 触发READER_IDLE和WRITER_IDLE事件.
  4. connectionManageHandler, 当连接状态发生改变时, 触发相应的事件, 并且处理IdleStateHandler触发的READER_IDLE和WRITER_IDLE事件.
  5. 进行消息派发的地方, 是我们接下来重点阐述的部分.

消息解码

NettyDecoder继承LengthFieldBasedFrameDecoder, LengthFieldBasedFrameDecoder根据消息内部的length field获得消息的长度.
具体不解释其工作原理, 以后可以详细分析.
下面时NettyDecoder的decode方法:

java
public Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
    ByteBuf frame = null;
    try {
        frame = (ByteBuf) super.decode(ctx, in);

        ByteBuffer byteBuffer = frame.nioBuffer();

        return RemotingCommand.decode(byteBuffer);
    } catch (Exception e) {
        RemotingUtil.closeChannel(ctx.channel());
    } finally {
        if (null != frame) {
            frame.release();
        }
    }

    return null;
}

为了简化, 我省略了部分校验等代码.
可以看到, 首先调用父类LengthFieldBasedFrameDecoder的decode方法, 将消息根据length取出, 然后调用RemotingCommand
的decode方法.

java
public static RemotingCommand decode(final ByteBuffer byteBuffer) {
    int length = byteBuffer.limit();
    int oriHeaderLen = byteBuffer.getInt();
    int headerLength = getHeaderLength(oriHeaderLen);

    byte[] headerData = new byte[headerLength];
    byteBuffer.get(headerData);

    RemotingCommand cmd = headerDecode(headerData, getProtocolType(oriHeaderLen));

    int bodyLength = length - 4 - headerLength;
    byte[] bodyData = null;
    if (bodyLength > 0) {
        bodyData = new byte[bodyLength];
        byteBuffer.get(bodyData);
    }
    cmd.body = bodyData;

    return cmd;
}

简要说明一下该方法的流程:

  1. 解码header.
  2. 将body简单从ByteBuffer取出放在字节数组中. (这里netty如果使用的是直接内存的话, 那么会发生一次copy?)

消息分发

接下来进入消息分发的步骤.
在此之前, 我们先了解一下broker在启动的时候注册处理器的相关逻辑.
在BrokerController中的registerProcessor方法里注册了大量相关处理器:

java
this.remotingServer.registerProcessor(RequestCode.SEND_MESSAGE, sendProcessor, this.sendMessageExecutor);
this.remotingServer.registerProcessor(RequestCode.SEND_MESSAGE_V2, sendProcessor, this.sendMessageExecutor);
this.remotingServer.registerProcessor(RequestCode.SEND_BATCH_MESSAGE, sendProcessor, this.sendMessageExecutor);
this.remotingServer.registerProcessor(RequestCode.CONSUMER_SEND_MSG_BACK, sendProcessor, this.sendMessageExecutor);

可以看到同时注册的还有线程池, 这样做可以保证各种业务不会互相影响.

接下来我们分析消息派发的逻辑:
NettyRemotingAbstract#processMessageReceived

java
public void processMessageReceived(ChannelHandlerContext ctx, RemotingCommand msg) {
    final RemotingCommand cmd = msg;
    if (cmd != null) {
        switch (cmd.getType()) {
            case REQUEST_COMMAND:
                processRequestCommand(ctx, cmd);
                break;
            case RESPONSE_COMMAND:
                processResponseCommand(ctx, cmd);
                break;
            default:
                break;
        }
    }
}

根据消息是请求还是响应分别进入不同逻辑中, 我们关注的是请求, 所以进入processRequestCommand方法中.
processRequestCommand的核心代码如下:

java
final Pair<NettyRequestProcessor, ExecutorService> pair = this.processorTable.get(cmd.getCode());
Runnable run = new Runnable() {
            @Override
            public void run() {
                final RemotingCommand response = pair.getObject1().processRequest(ctx, cmd);
                ctx.writeAndFlush(response);
        };
final RequestTask requestTask = new RequestTask(run, ctx.channel(), cmd);
pair.getObject2().submit(requestTask);

很简单, 就是根据命令code从先前注册的code -> processor, executor 映射中找到对应的processor,
然后在对应的线程池中使用找出来的processor进行消息的处理.

CommitLog消息存储

有了以上的基础, 我们现在开始真正的分析.

消息处理

在SendMessageProcessor中的sendMessage方法, 其核心代码如下:

java
private RemotingCommand sendMessage(final ChannelHandlerContext ctx, // 用于获取channel相关信息
                                        final RemotingCommand request, // 消息请求, 主要使用里面的body
                                        final SendMessageContext sendMessageContext, // 开启消息跟踪时的消息上下文信息
                                        final SendMessageRequestHeader requestHeader) { // 用于获取消息的相关信息,比如topic等
    // 检查topic是否可写, 如果topic可自动且topic不存在的情况下则创建topic, 否则返回TOPIC_NOT_EXIST异常
    // 进行queueId的范围校验等
    super.msgCheck(ctx, requestHeader, response);
    // MessageExtBrokerInner是消息在broker内部的表现形式, 其中不仅包含body也包含topic ,queueId等信息
    MessageExtBrokerInner msgInner = new MessageExtBrokerInner();
    msgInner.setTopic(requestHeader.getTopic());
    msgInner.setQueueId(queueIdInt);
    msgInner.setBody(body);
    // 消费端消费失败时, 会将重试消息发到所属consumerGroup的retry topic
    // 这里handleRetryAndDLQ就是用来控制重试次数的, 如果重试次数超过最大重试次数时,
    // 会进入DLQ死信队列
    if (!handleRetryAndDLQ(requestHeader, response, request, msgInner, topicConfig)) {
        return response;
    }
    // 将消息存储到MessageStore中
    putMessageResult = this.brokerController.getMessageStore().putMessage(msgInner);
    // 对存储结果进行处理 TODO
    return handlePutMessageResult(putMessageResult, response, request, msgInner, responseHeader, sendMessageContext, ctx, queueIdInt);
}

CommitLog的putMessage方法解析

MessageStore的putMessage就是存储消息的核心代码了, MessageStore在rocketmq内的实现DefaultMessageStore采用一个CommitLog
的结构来存储消息, 所有topic的消息都存放在一个CommitLog里.

CommitLog以文件形式进行存储, 其中含有一个文件队列: MappedFileQueue, 消息写入是顺序写入.
Commitlog文件存储目录为${ROCKET_HOME}/store/commitlog目录,每一个文件默认1G,一个文件写满后再创建另外一个,
以该文件中第一个偏移量为文件名,偏移量小于20位用0补齐。

在写入CommitLog的时候会加锁, 所以写入CommitLog的过程是单线程的.
下面的代码在CommitLog类中putMessage方法中.

java
long beginLockTimestamp = this.defaultMessageStore.getSystemClock().now();
this.beginTimeInLock = beginLockTimestamp;

// Here settings are stored timestamp, in order to ensure an orderly
// global
msg.setStoreTimestamp(beginLockTimestamp);

if (null == mappedFile || mappedFile.isFull()) {
    mappedFile = this.mappedFileQueue.getLastMappedFile(0); // Mark: NewFile may be cause noise
}
if (null == mappedFile) {
    log.error("create mapped file1 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString());
    beginTimeInLock = 0;
    return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, null);
}

要进行顺序写消息的话就必须获取当前最后的文件来进行append, 所以上面的代码首先尝试获取最后一个mappedFile, 如果为空或者已满, 那么会使用getLastMappedFile(0)方法来新建一个mappedFile.
具体如何新建mappedFile在稍后进行解释.

接下来的逻辑就是否直接了, 只不过有对mappedFile空间不够的处理

java
result = mappedFile.appendMessage(msg, this.appendMessageCallback);
switch (result.getStatus()) {
    case PUT_OK:
        break;
    case END_OF_FILE:
        unlockMappedFile = mappedFile;
        // Create a new file, re-write the message
        mappedFile = this.mappedFileQueue.getLastMappedFile(0);
        if (null == mappedFile) {
            // XXX: warn and notify me
            log.error("create mapped file2 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString());
            beginTimeInLock = 0;
            return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, result);
        }
        result = mappedFile.appendMessage(msg, this.appendMessageCallback);
        break;
        ...
}

根据mappedFile的appendMessage方法的结果进行处理, 如果是EOF, 那么意味着需要新建一个mappedFile
来进行append.

值得注意的是在EOF的处理中有这个变量:unlockMappedFile, 它的作用是稍后会将它从内存中解除锁定:

java
if (null != unlockMappedFile && this.defaultMessageStore.getMessageStoreConfig().isWarmMapedFileEnable()) {
    this.defaultMessageStore.unlockMappedFile(unlockMappedFile);
}
// MappedFile.java#munlock
public void munlock() {
    final long beginTime = System.currentTimeMillis();
    final long address = ((DirectBuffer) (this.mappedByteBuffer)).address();
    Pointer pointer = new Pointer(address);
    int ret = LibC.INSTANCE.munlock(pointer, new NativeLong(this.fileSize));
    log.info("munlock {} {} {} ret = {} time consuming = {}", address, this.fileName, this.fileSize, ret, System.currentTimeMillis() - beginTime);
}

注意此时仅仅是将内容写到ByteBuffer中, 还没有flush到磁盘中, 稍后会解释这一异步操作.
handleDiskFlush(result, putMessageResult, msg);
接下来Master写入成功, 那么还有写入从broker中:
handleHA(result, putMessageResult, msg);
关于HA同步我们有机会再介绍.

以上到工作做完之后接下来就是返回一个putMessageResult了:

java
PutMessageResult putMessageResult = new PutMessageResult(PutMessageStatus.PUT_OK, result);
return putMessageResult;

我们总结一下上述流程:

  1. 写入之前先确保mappedFile存在
    2, 写入mappedFile, 同时对EOF情况做处理
  2. 如果是同步刷盘模式下需要进行flush
  3. 如果是同步复制的模式的话要进行HA同步操作
  4. 返回PUT_OK结果

在CommitLog的存储就是以上这些, 在前面遗留了几个比较重要问题:

  1. MappedFile的创建
  2. MappedFile写入消息和刷盘的详细实现

MappedFile的创建

我们先来看上面的一个问题MappedFile的创建.
代码在MappedFileQueue.java的getLastMappedFile方法

java
public MappedFile getLastMappedFile(final long startOffset, boolean needCreate) {
    long createOffset = -1;
    MappedFile mappedFileLast = getLastMappedFile();

    if (mappedFileLast == null) {
        createOffset = startOffset - (startOffset % this.mappedFileSize);
    }

    if (mappedFileLast != null && mappedFileLast.isFull()) {
        createOffset = mappedFileLast.getFileFromOffset() + this.mappedFileSize;
    }

    if (createOffset != -1 && needCreate) {
        String nextFilePath = this.storePath + File.separator + UtilAll.offset2FileName(createOffset);
        String nextNextFilePath = this.storePath + File.separator
            + UtilAll.offset2FileName(createOffset + this.mappedFileSize);
        MappedFile mappedFile = null;

        if (this.allocateMappedFileService != null) {
            mappedFile = this.allocateMappedFileService.putRequestAndReturnMappedFile(nextFilePath,
                nextNextFilePath, this.mappedFileSize);
        } else {
            try {
                mappedFile = new MappedFile(nextFilePath, this.mappedFileSize);
            } catch (IOException e) {
                log.error("create mappedFile exception", e);
            }
        }

        if (mappedFile != null) {
            if (this.mappedFiles.isEmpty()) {
                mappedFile.setFirstCreateInQueue(true);
            }
            this.mappedFiles.add(mappedFile);
        }

        return mappedFile;
    }

    return mappedFileLast;
}

在上一节的分析中, 我们知道是这样调用的:getLastMappedFile(0, true).

  1. 文件名的确定
    传入startOffset的作用是确定mappedFile的名字, 名字是通过createOffset控制的.
    首先如果当前没有文件, 那么通过startOffset与mappedFile的文件大小进行对齐得到createOffset即可.
    如果mappedFile不为空且没满, 那么createOffset =- 1不变, 直接返回.
    如果已满那么根据这个满的mappedFile得到下一个文件的起始offset.

  2. 文件的真正创建
    判断allocateMappedFileService是否为空.(按当前的实现, 是不可能为空的, 所以下面仅分析不为空的情况)
    接着调用allocateMappedFileService的putRequestAndReturnMappedFile方法创建一个mappedFile.

核心代码如下所示:
AllocateMappedFileService.java#mmapOperation方法

java
MappedFile mappedFile;
if (messageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
    try {
        mappedFile = ServiceLoader.load(MappedFile.class).iterator().next();
        mappedFile.init(req.getFilePath(), req.getFileSize(), messageStore.getTransientStorePool());
    } catch (RuntimeException e) {
        log.warn("Use default implementation.");
        mappedFile = new MappedFile(req.getFilePath(), req.getFileSize(), messageStore.getTransientStorePool());
    }
} else {
    mappedFile = new MappedFile(req.getFilePath(), req.getFileSize());
}

这里解释一下TransientStorePoolEnable选项的作用, 如果开启了TransientStorePoolEnable选项的话,
broker会创建一个堆外buffer池(默认5个buffer, 每个1G), 同时将它们锁定在内存中(使用jna).
这样在消息写入commitLog时会有一些不同: 先将消息写入到这个堆外内存中, 然后再commit到FileChannel的
OS Page Cache中, 最后再flush到磁盘里.不开启这个选项的话是没有commit的步骤直接写入到FileChannel映射的
OS Page Cache中.

注意, TransientStorePoolEnable仅在开启异步刷盘的时候才有效, 因为如果是同步的话那么就没必要使用堆外内存了,
因为每次处理消息都要直接flush到磁盘上.

值得一提的是, 在AllocateMappedFileService新建mappedFile到时候会根据是否开启warmMapedFileEnable
这个选项来决定是否预先向FileChannel映射到ByteBuffer中写入数据来保证不发生缺页中断.(注意这里处理的是FileChannel的映射的
逻辑地址而不是上面TransientStorePool的堆外内存的地址, TransientStorePool在创建池内的buffer的时候就已经处理过了),
因为mmap只是创建了逻辑地址到磁盘文件到映射关系, 真正的从磁盘copy到内核缓冲区还没有进行, 需要等到实际对mmap映射到逻辑地址进行读写时
才会发生缺页中断, 去磁盘中copy, 所以这里预先在每个页大小写入一个字节就是为了提前将数据装入物理内存而不会在运行时发生缺页中断.

消息append

入口代码在MappedFile.java的appendMessagesInner方法中:

java
public AppendMessageResult appendMessagesInner(final MessageExt messageExt, final AppendMessageCallback cb) {
    int currentPos = this.wrotePosition.get();

    if (currentPos < this.fileSize) {
        // 首先我们要确定消息首先写入哪里? 如果我们开启了TransientStorePoolEnable那么就需要写入到writeBuffer——这是我们之前
        // 从TransientStorePool中borrow的buffer, 否则就写入到文件映射buffer中
        ByteBuffer byteBuffer = writeBuffer != null ? writeBuffer.slice() : this.mappedByteBuffer.slice();
        byteBuffer.position(currentPos);
        AppendMessageResult result;
        if (messageExt instanceof MessageExtBrokerInner) {
            // 这里是消息append的核心实现
            result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBrokerInner) messageExt);
        } else if (messageExt instanceof MessageExtBatch) {
            result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBatch) messageExt);
        } else {
            return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR);
        }
        this.wrotePosition.addAndGet(result.getWroteBytes());
        this.storeTimestamp = result.getStoreTimestamp();
        return result;
    }
    log.error("MappedFile.appendMessage return null, wrotePosition: {} fileSize: {}", currentPos, this.fileSize);
    return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR);
}

参数的AppendMessageCallback会传入CommitLog的内部类DefautlAppendMessageCallback
接下来我们分析DefautlAppendMessageCallback的关于消息append的核心代码.

DefautlAppendMessageCallback关于消息写入的方法是doAppend方法, 由于其代码比较多, 我在这里先简要
概括一下它的大概流程:(其中有针对事务消息的特殊处理, 由于不是分析的主要目标就不详细说明了)

  1. 确保文件空间足够, 如果不够则返回EOF
  2. 将消息的相关信息和body写入到DefautlAppendMessageCallback的成员变量中的msgStoreItemMemory
  3. 将msgStoreItemMemory的内容put到mappedFile的buffer中.

消息commit以及刷盘分析

我们回到CommitLog的putMessage方法中, 根据之前的分析我们知道在消息put到mappedFile后会进行
commit和flush到处理, 现在我们来分析一下.

在CommitLog中有两个Service是用来处理commit和flush的:

java
private final FlushCommitLogService flushCommitLogService;
private final FlushCommitLogService commitLogService = new CommitRealTimeService();

flushCommitLogService用来处理flush, commitLogService用来处理commit.对于commit和flush的区别上面已经说明了.

其中flushCommitLogService根据刷盘模式有不同的实现:

java
if (FlushDiskType.SYNC_FLUSH == defaultMessageStore.getMessageStoreConfig().getFlushDiskType()) {
    this.flushCommitLogService = new GroupCommitService();
} else {
    this.flushCommitLogService = new FlushRealTimeService();
}

接着我们看一下handleDiskFlush方法的具体实现:

java
public void handleDiskFlush(AppendMessageResult result, PutMessageResult putMessageResult, MessageExt messageExt) {
    // 同步刷盘, flushCommitLogService使用GroupCommitService实现, 调用GroupCommitRequest的waitForFlush方法
    // 同步等待刷盘
    if (FlushDiskType.SYNC_FLUSH == this.defaultMessageStore.getMessageStoreConfig().getFlushDiskType()) {
        if (messageExt.isWaitStoreMsgOK()) {
            GroupCommitRequest request = new GroupCommitRequest(result.getWroteOffset() + result.getWroteBytes());
            service.putRequest(request);
            boolean flushOK = request.waitForFlush(this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout());
            ...
        } else {
            service.wakeup();
        }
    }
    // 异步刷盘, 如果开启了TransientStorePoolEnable的话会先commit
    else {
        if (!this.defaultMessageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
            flushCommitLogService.wakeup();
        } else {
            commitLogService.wakeup();
        }
    }
}

消息commit的具体分析

代码在CommitRealTimeService的run方法中.
核心代码如下:

java
boolean result = CommitLog.this.mappedFileQueue.commit(commitDataLeastPages);

猜想如下:
调用mappedFileQueue的commit方法, 应是通过getLastMappedFile获取当前的mappedFile,
再调用mappedFile的commit方法, mappedFile的commit方法的逻辑应该是读取writeBuffer的flushPos到
writePos之间的数据写入到mappedByteBuffer中.

如下是appedFileQueue的commit方法

java
public boolean commit(final int commitLeastPages) {
    boolean result = true;
    MappedFile mappedFile = this.findMappedFileByOffset(this.committedWhere, this.committedWhere == 0);
    if (mappedFile != null) {
        int offset = mappedFile.commit(commitLeastPages);
        long where = mappedFile.getFileFromOffset() + offset;
        result = where == this.committedWhere;
        this.committedWhere = where;
    }

    return result;
}

与我到猜想大致相同, 但是不是获取最后一个file, 而是通过记录的上一次commitOffset获取的file.

接下来我们看mappedFile的commit方法:

java
public int commit(final int commitLeastPages) {
    if (writeBuffer == null) {
        // 如果writeBuffer为空说明没有开启TransientPool, 不需要commit, 直接返回即可
        return this.wrotePosition.get();
    }
    // isAbleToCommit方法用来判断当前脏数据是否满足commitLeastPages的需求
    if (this.isAbleToCommit(commitLeastPages)) {
        if (this.hold()) {
            commit0(commitLeastPages);
            this.release();
        } else {
            log.warn("in commit, hold failed, commit offset = " + this.committedPosition.get());
        }
    }

    // 当前writeBuffer的所有脏数据都写到FileChannel上了, 那么需要新建mappedFile, 重新向TransientPool申请一个
    // writeBuffer, 此时将writeBuffer归还给pool
    if (writeBuffer != null && this.transientStorePool != null && this.fileSize == this.committedPosition.get()) {
        this.transientStorePool.returnBuffer(writeBuffer);
        this.writeBuffer = null;
    }

    return this.committedPosition.get();
}

protected void commit0(final int commitLeastPages) {
    int writePos = this.wrotePosition.get();
    int lastCommittedPosition = this.committedPosition.get();

    if (writePos - this.committedPosition.get() > 0) {
        try {
            ByteBuffer byteBuffer = writeBuffer.slice();
            // 此处确实如我所料, 将上次commit的pos到wrotePos之间的数据写入到FileChannel中
            byteBuffer.position(lastCommittedPosition);
            byteBuffer.limit(writePos);
            // 这里使用FileChannel的write写入,实际上是
            // 系统调用, 只不过由于刷盘的条件以满足脏数据大小大于4页
            // 为什么要与读的mappedByteBuffer分离? 这是为了实现一种读写分离的机制:
            // 即读使用PageCache, 写先写入堆外内存, 然后再以一定间隔频率写入PageCache, 而不是
            // 每次刷盘都写入PageCache, 这样会造成PageCache繁忙(对PageCache具体造成什么影响暂时还不清楚)
            this.fileChannel.position(lastCommittedPosition);
            this.fileChannel.write(byteBuffer);
            this.committedPosition.set(writePos);
        } catch (Throwable e) {
            log.error("Error occurred when commit data to FileChannel.", e);
        }
    }
}

整体的逻辑与我之前预期的差不多.

消息刷盘的具体分析

我们直接看mappedFile的flush方法:

java
public int flush(final int flushLeastPages) {
    // 判断当前数据是否符合flushLeastPages的要求
    if (this.isAbleToFlush(flushLeastPages)) {
        if (this.hold()) {
            int value = getReadPosition();

            try {
                // 使用TransientPool是直接写入到FileChannel中的, 所以使用fileChannel的force方法
                if (writeBuffer != null || this.fileChannel.position() != 0) {
                    this.fileChannel.force(false);
                } else {
                    this.mappedByteBuffer.force();
                }
            } catch (Throwable e) {
                log.error("Error occurred when force data to disk.", e);
            }
            // 刷新flushedPos, 这里的value指的是当前commitPos, 
            // 如果使用TransientPool, 那么value就是commitPos
            // 否则就是当前的wrotePos
            this.flushedPosition.set(value);
            this.release();
        } else {
            log.warn("in flush, hold failed, flush offset = " + this.flushedPosition.get());
            this.flushedPosition.set(getReadPosition());
        }
    }
    return this.getFlushedPosition();
}

ConsumeQueue

RocketMQ基于主题订阅模式实现消息消费,消费者关心的是一个主题下的所有消息,但由于同一主题的消息不连续地存储在commitlog文件中,试想一下如果消息消费者直接从消息存储文件(commitlog)中去遍历查找订阅主题下的消息,效率将极其低下,RocketMQ为了适应消息消费的检索需求,设计了消息消费队列文件(Consumequeue),该文件可以看成是Commitlog关于消息消费的“索引”文件,consumequeue的第一级目录为消息主题,第二级目录为主题的消息队列.

consume queue 条目结构如下所示:

text
commitlog offset   +    size    +    tag hashcode
-----8bytes-----    ---4bytes--- ------8bytes-----

关于consume queue的写入核心代码如下所示, 首先是ReputMessageService,:

java
SelectMappedBufferResult result = DefaultMessageStore.this.commitLog.getData(reputFromOffset);
for (int readSize = 0; readSize < result.getSize() && doNext; ) {
// 构造DispatchRequest对象
 DispatchRequest dispatchRequest =
    DefaultMessageStore.this.commitLog.checkMessageAndReturnSize(result.getByteBuffer(), false, false);
int size = dispatchRequest.getBufferSize() == -1 ? dispatchRequest.getMsgSize() : dispatchRequest.getBufferSize();

if (dispatchRequest.isSuccess()) {
    if (size > 0) {
        // 真正进行分发的地方
        DefaultMessageStore.this.doDispatch(dispatchRequest);
        ...
        this.reputFromOffset += size;
        readSize += size;
        ...
    } else if (size == 0) {
        // 还记得当commitlog的mappedFile最后空间不够了怎么办吗? 👆(在之前有相关解释)
        this.reputFromOffset = DefaultMessageStore.this.commitLog.rollNextFile(this.reputFromOffset);
        readSize = result.getSize();
    }
}
}

该service thread每隔1ms会从当前reputFromOffset开始从commitlog中将消息信息取出放入
consumequeue和indexFile中.
它会构造一个DispatchRequest对象, 里面包含消息的相关信息:

java
private final String topic;
private final int queueId;
private final long commitLogOffset;
private int msgSize;
private final long tagsCode;
private final long storeTimestamp;
private final long consumeQueueOffset;
private final String keys;
private final boolean success;
private final String uniqKey;

private final int sysFlag;
private final long preparedTransactionOffset;
private final Map<String, String> propertiesMap;
private byte[] bitMap;

接下来我们看DefaultMessageStore的doDispatch方法

java
public void doDispatch(DispatchRequest req) {
    for (CommitLogDispatcher dispatcher : this.dispatcherList) {
        dispatcher.dispatch(req);
    }
}

在DefaultMessageStore的构造方法中初始化了两个分发器,分别是consumequeue的和indexFile的, 我们先看consume queue的:

java
this.dispatcherList = new LinkedList<>();
this.dispatcherList.addLast(new CommitLogDispatcherBuildConsumeQueue());
this.dispatcherList.addLast(new CommitLogDispatcherBuildIndex());

我们先猜想一下实现逻辑:

  1. 首先从consume queue的mappedFileQueue中获得最后一个file
  2. 将DispatchRequest对象中的物理位移, size和tagscode取出, 放入到file中, 至于是否刷盘尚不清楚.

好的现在我们来看对应的代码吧:(DefaultMessageStore.java#putMessagePositionInfo)

java
public void putMessagePositionInfo(DispatchRequest dispatchRequest) {
    ConsumeQueue cq = this.findConsumeQueue(dispatchRequest.getTopic(), dispatchRequest.getQueueId());
    cq.putMessagePositionInfoWrapper(dispatchRequest);
}

如上所示, 先找到对应的consume queue对象, 然后将dispatchRequest放入其中.
findConsumeQueue逻辑非常简单, 就是根据topic和queueid获取相应的记录即可.
下面我们看真正进行写入的逻辑:(DefaultMessageStore的putMessagePositionInfo方法)

java
this.byteBufferIndex.flip();
this.byteBufferIndex.limit(CQ_STORE_UNIT_SIZE);
this.byteBufferIndex.putLong(offset);
this.byteBufferIndex.putInt(size);
this.byteBufferIndex.putLong(tagsCode);

MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile(expectLogicOffset);
this.maxPhysicOffset = offset + size;
return mappedFile.appendMessage(this.byteBufferIndex.array());

可以看到,我们猜想的逻辑与代码中几乎一致, 当然我在这里省略了一些代码, 但大致的逻辑是这样的.


File: _posts/2019-09-08-operatingsystem-process.md

[Binary file]


File: _posts/2019-09-15-rocketmq-indexfile.md


layout: post title: "Rocketmq IndexFile源码解读" date: 2019-09-15 10:02:00 +0700 categories: [rocketmq]

学习目标

  1. 搞清楚rocketmq中IndexFile机制原理, 了解IndexFile是什么以及它的作用, 组成原理以及运行原理.

IndexFile的作用

IndexFile的主要作用是作为CommitLog的索引文件, 可以快速根据给定的key查找到对应消息在CommitLog的物理位移.

IndexFile的组成

IndexFile主要由一下三个部分组成:
IndexHeader + HashSlot + IndexItem

IndexHeader:
IndexFile的头部, 大小40字节.

  • beginTimestamp: 该IndexFile文件中包含消息的最小存储时间.
  • endTimeStamp: 该IndexFile文件中包含消息的最大存储时间.
  • beginPhyoffset: 该IndexFile文件中包含消息的最小物理位移.
  • endPhyoffset: 该IndexFile文件中包含消息的最大物理位移.
  • hashSlotcount: 该IndexFile文件中包含的hashSlot的总数.
  • indexCount: 该IndexFile文件中使用的Index条目个数.

HashSlot:
默认包含500w个hash槽, 每个hash槽中存储的是最近一个hash条目的位置.

IndexItem:
默认最多包含2000w个indexItem.
其组成如下所示:

  • keyHash: 消息key的hash, 当根据key搜索时比较的是其hash, 所以在之后会比较key本身.
  • phyOffset: 消息的物理位移.
  • timeDiff: 与第一条消息的时间戳差值.
  • prevIndex: hash冲突的上一个indexItem的位置.

Hash冲突解决方案

IndexFile是如何解决hash冲突的呢?

如果发生Hash冲突, 首先查看hashSlot存储的值, 该值是当前发生冲突的hash的IndexItem的位置, 然后
使用当前hashSlotcount + 1的值覆盖该hashSlot.

在Item区新建一个索引条目, 要注意prevIndex, 这里存放的是之前从hashslot中拿到的值, 这样就组成了一条映射到同一个slot上的链.
只要找到了hashSlot的值, 再根据prevIndex构成的链式结构就一定能够找到对应key的消息的物理位移.

实现代码如下(IndexFile的putKey方法):

  1. 根据key的hash值找到对应的hashSlot, 确定上一条相同slot在IndexFile中的绝对位移:
java
int keyHash = indexKeyHashMethod(key);
int slotPos = keyHash % this.hashSlotNum;
int absSlotPos = IndexHeader.INDEX_HEADER_SIZE + slotPos * hashSlotSize;
  1. 确定本次要插入的位置, 可以看到, 插入位置是不断在IndexFile的尾部进行append.
java
int absIndexPos =
      IndexHeader.INDEX_HEADER_SIZE + this.hashSlotNum * hashSlotSize
      + this.indexHeader.getIndexCount() * indexSize;
  1. 进行插入
java
// hash
this.mappedByteBuffer.putInt(absIndexPos, keyHash);
// 物理位移
this.mappedByteBuffer.putLong(absIndexPos + 4, phyOffset);
// 与beginTimestamp相对的时间位移
this.mappedByteBuffer.putInt(absIndexPos + 4 + 8, (int) timeDiff);
// 记录“链表”的上一个indexItem的位置
this.mappedByteBuffer.putInt(absIndexPos + 4 + 8 + 4, slotValue);

// 更新hashslot的值, 将其值设置为本次插入的位置(位置指第几个indexItem)
this.mappedByteBuffer.putInt(absSlotPos, this.indexHeader.getIndexCount());

根据key定位条目物理位移流程源码解读

该流程的代码主要在IndexFile的selectPhyOffset方法内, 该方法有以下几个参数:

  • key: 根据该key查找对应的消息
  • maxNum: 本次查找返回最多的消息个数
  • begin: 查找该时间戳以后的消息
  • end: 查找该时间戳以前的消息
  1. 首先确定key的hash值并且根据hashSlot找到该hash值对应的最新的IndexItem的位置.
java
int keyHash = indexKeyHashMethod(key);
int slotPos = keyHash % this.hashSlotNum;
int absSlotPos = IndexHeader.INDEX_HEADER_SIZE + slotPos * hashSlotSize;
  1. 根据prevIndex组成的链式结构, 将符合条件的消息的物理位移加入到结果中
java
int absIndexPos =
      IndexHeader.INDEX_HEADER_SIZE + this.hashSlotNum * hashSlotSize
            + nextIndexToRead * indexSize;

int keyHashRead = this.mappedByteBuffer.getInt(absIndexPos);
long phyOffsetRead = this.mappedByteBuffer.getLong(absIndexPos + 4);

long timeDiff = (long) this.mappedByteBuffer.getInt(absIndexPos + 4 + 8);
int prevIndexRead = this.mappedByteBuffer.getInt(absIndexPos + 4 + 8 + 4);

if (timeDiff < 0) {
      break;
}

timeDiff *= 1000L;

long timeRead = this.indexHeader.getBeginTimestamp() + timeDiff;
boolean timeMatched = (timeRead >= begin) && (timeRead <= end);

if (keyHash == keyHashRead && timeMatched) {
      phyOffsets.add(phyOffsetRead);
}

if (prevIndexRead <= invalidIndex
      || prevIndexRead > this.indexHeader.getIndexCount()
      || prevIndexRead == nextIndexToRead || timeRead < begin) {
      break;
}

File: _posts/2019-09-16-operatingsystem-thread.md


layout: post title: "操作系统: 线程相关概念简介" date: 2019-09-16 20:02:00 +0700 categories: [os]

学习目标

  1. 搞清楚线程是什么以及它的作用, 线程实现的原理.

为什么需要线程?

  1. 线程让一个进程内的多个流程的同时运行成为了可能, 而且同一个进程中的线程可以共享进程的地址空间和其中的数据.
  2. 线程比进程更加的轻量, 创建线程的代价比创建进程的代价要小的多.

经典线程模型

进程与线程的区别是什么?
进程是进行资源分组的手段. 一个进程拥有单独的地址空间, 包括它打开的文件, 子进程都是它的资源.
而线程则是执行的体现, 每个线程都有自己的PC, 栈和变量寄存器.

每个线程都有自己的栈, 每个线程之间互不影响.

POSIX下的线程

POSIX定义了很多关于线程的函数, 这里我们举几个例子.

  • Pthread_create 创建一个新线程, 返回新线程的标识符
  • Pthread_exit 终止一个线程
  • Pthread_join 等待另一个线程退出
  • Pthread_yield 让出cpu给其他线程运行

线程的实现

下面我们讨论一下线程的实现方式, 主要有以下两大类: 1. 在用户空间实现 2. 在内核中实现

在用户空间实现线程

一种方式是在用户空间实现线程. 在内核中仍然认为进程是一个单线程的, 但是在用户空间为每一个进程都分配一个thread table. 由用户空间的
系统来管理线程的状态(运行或阻塞), 由于在用户空间进行线程的调度管理, 这使得线程的调度十分快几乎没有消耗; 然而缺点也是明显的: 一旦一个线程出现阻塞
系统调用这时候该实现就无法解决了, 而且对于线程饥饿问题该模型也不好解决, 同时最致命的问题在于该模型的性能伸缩性极差, 当一个计算密集型的程序运行时
似乎多线程并没起到什么作用.

在内核实现线程

在内核维护线程表, 所有线程的创建销毁都由系统调用实现, 内核的线程表保存着每个线程的寄存器和状态和栈信息.
使用内核实现线程解决了上面用户线程的一些问题, 比如:如果有线程出现阻塞, 那么调度器只需要去线程表中查看是否有可以运行的线程就可以,
线程饥饿问题也能解决了, 而且伸缩性也非常高.

混合实现

结合用户线程和内核线程的优点, 在用户空间的多个线程绑定到内核线程的一个线程上.


File: _posts/2019-09-20-jvm-gc-problem.md

[Binary file]


File: _posts/2019-09-20-kube-cluster-create.md


layout: post title: "K8s集群搭建步骤: 单master" date: 2019-09-20 20:02:00 +0700 categories: [k8s]

K8s集群搭建步骤

安装必要软件

K8s的master和worker节点都需要安装如下软件:docker,kubeadm,kubelet,kubectl
脚本如下所示:
卸载旧版本

text
yum remove -y docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-selinux \
docker-engine-selinux \
docker-engine

设置 yum repository

text
yum install -y yum-utils \
device-mapper-persistent-data \
lvm2
yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

安装并启动 docker

text
yum install -y docker-ce-18.09.7 docker-ce-cli-18.09.7 containerd.io
systemctl enable docker
systemctl start docker

安装 nfs-utils

必须先安装 nfs-utils 才能挂载 nfs 网络存储

text
yum install -y nfs-utils

关闭 防火墙

text
systemctl stop firewalld
systemctl disable firewalld

关闭 SeLinux

text
setenforce 0
sed -i "s/SELINUX=enforcing/SELINUX=disabled/g" /etc/selinux/config

关闭 swap

text
swapoff -a
yes | cp /etc/fstab /etc/fstab_bak
cat /etc/fstab_bak |grep -v swap > /etc/fstab

修改 /etc/sysctl.conf

如果有配置,则修改

text
sed -i "s#^net.ipv4.ip_forward.*#net.ipv4.ip_forward=1#g"  /etc/sysctl.conf
sed -i "s#^net.bridge.bridge-nf-call-ip6tables.*#net.bridge.bridge-nf-call-ip6tables=1#g"  /etc/sysctl.conf
sed -i "s#^net.bridge.bridge-nf-call-iptables.*#net.bridge.bridge-nf-call-iptables=1#g"  /etc/sysctl.conf

可能没有,追加

text
echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.conf
echo "net.bridge.bridge-nf-call-ip6tables = 1" >> /etc/sysctl.conf
echo "net.bridge.bridge-nf-call-iptables = 1" >> /etc/sysctl.conf

执行命令以应用

text
sysctl -p

配置K8S的yum源

text
cat <<EOF > /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=http://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64
enabled=1
gpgcheck=0
repo_gpgcheck=0
gpgkey=http://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg
       http://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg
EOF

卸载旧版本

yum remove -y kubelet kubeadm kubectl

安装kubelet、kubeadm、kubectl

text
yum install -y kubelet-1.15.3 kubeadm-1.15.3 kubectl-1.15.3

# 修改docker Cgroup Driver为systemd
# # 将/usr/lib/systemd/system/docker.service文件中的这一行 ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
# # 修改为 ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock --exec-opt native.cgroupdriver=systemd
# 如果不修改,在添加 worker 节点时可能会碰到如下错误
# [WARNING IsDockerSystemdCheck]: detected "cgroupfs" as the Docker cgroup driver. The recommended driver is "systemd". 
# Please follow the guide at https://kubernetes.io/docs/setup/cri/
sed -i "s#^ExecStart=/usr/bin/dockerd.*#ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock --exec-opt native.cgroupdriver=systemd#g" /usr/lib/systemd/system/docker.service

# 设置 docker 镜像,提高 docker 镜像下载速度和稳定性
# 如果您访问 https://hub.docker.io 速度非常稳定,亦可以跳过这个步骤
curl -sSL https://get.daocloud.io/daotools/set_mirror.sh | sh -s http://f1361db2.m.daocloud.io

# 重启 docker,并启动 kubelet
systemctl daemon-reload
systemctl restart docker
systemctl enable kubelet && systemctl start kubelet

docker version
2. 使用kubeadm在master机器上初始化集群
使用kubeadm初始化集群  
apiVersion: kubeadm.k8s.io/v1beta2
kind: ClusterConfiguration
kubernetesVersion: v1.15.3
imageRepository: registry.cn-hangzhou.aliyuncs.com/google_containers
controlPlaneEndpoint: "172.31.129.129:6443"
networking:
  serviceSubnet: "10.96.0.0/12"
  podSubnet: "10.244.0.0/16"
  dnsDomain: "cluster.local"
其中要注意:

controlPlaneEndpoint填写master的ip。
podSubnet不能和master节点/worker节点重叠。(podSubnet之后还要使用)
初始化结束后,在master上执行如下命令,让kubectl可用:
配置kubectl  
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
配置好kubectl后,执行:
kubectl get pod -n kube-system
查看master上pod的状态,正常应该是全部为Running。
使用kubectl get node查看机器节点的状况:
 
目前只有一个节点即master节点。
3. 配置Pod网络
我们使用calico部署k8s集群网络。
先获取calico.yaml: wget https://docs.projectcalico.org/v3.8/manifests/calico.yaml
执行命令: sed -i "s#192\.168\.0\.0/16#${POD_SUBNET}#" calico.yaml
其中${POD_SUBNET}替换为上面配置master时的podSubnet。
然后执行:kubectl apply -f calico.yaml

4. 将worker节点加入集群
worker节点安装好必要的软件之后,我们就可以开始加入到集群中了。
首先获取加入到集群中的命令,在master上执行:kubeadm token create --print-join-command
得到类似如下的命令:
配置kubectl  
kubeadm join 172.31.129.129:6443 --token 5h5stg.rmjosu2a3wbhz2ql     --discovery-token-ca-cert-hash sha256:ad066deabfd7c55482f8d02f3bc35bdaf95060fca021ec6b68f551009195a582
在worker上执行即可。
然后在master上执行kubectl get node命令观察节点的状态是否都是Ready。
之后再执行get pod命令观察calico是否是Running状态,如果出现CrashBackoff状态那么可能是calico获取的网络接口不对,
整calicao 网络插件的网卡发现机制,修改IP_AUTODETECTION_METHOD对应的value值。官方提供的yaml文件中,ip识别策略(IPDETECTMETHOD)没有配置,即默认为first-found,这会导致一个网络异常的ip作为nodeIP被注册,从而影响node-to-node mesh。我们可以修改成can-reach或者interface的策略,尝试连接某一个Ready的node的IP,以此选择出正确的IP。
配置kubectl  
// calico.yaml 文件添加以下二行
- name: IP_AUTODETECTION_METHOD
value: "interface=eth.*"  #  根据实际网卡开头配置
 
 // 配置如下             
- name: CLUSTER_TYPE
  value: "k8s,bgp"
# Auto-detect the BGP IP address.
- name: IP
  value: "autodetect"
- name: IP_AUTODETECTION_METHOD
  value: "interface=ens.*"
# Enable IPIP
- name: CALICO_IPV4POOL_IPIP
  value: "Always"
Optional可选步骤:
安装dashboard
首先获取dashboard的yaml文件,然后对其进行改动使其可以由外部根据端口直接访问,否则还需要启动一个kube-proxy进行代理比较麻烦。
改动如下:
配置kubectl  
ports:
- containerPort: 8443
protocol: TCP
name: https
args:
- --auto-generate-certificates
- --token-ttl=43200


...


kind: Service
apiVersion: v1
metadata:
  labels:
    k8s-app: kubernetes-dashboard
  name: kubernetes-dashboard
  namespace: kube-system
spec:
  ports:
    - port: 443
      targetPort: 8443
      nodePort: 30001
      name: https
  type: NodePort
  selector:
    k8s-app: kubernetes-dashboard

如上,我们将dashboard的服务类型改为NodePort,这样它就可以直接由外部的port访问。
我们配置了--token-ttl=43200设置token的失效时间。
登录在浏览器中输入https://master_ip:30001即可访问,但是需要输入登录凭证,这里我们使用token登录,token配置如下。

配置token
接着我们需要配置一个用户来获取它的token,配置文件如下:
配置kubectl

text
apiVersion: v1
kind: ServiceAccount
metadata:
name: admin-user
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: admin-user
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: admin-user
namespace: kube-system

使用apply -f创建一个名为admin-user的ServiceAccount和角色之间的绑定关系。
然后我们使用如下命令获取该用户的token:
kubectl -n kube-system describe $(kubectl -n kube-system get secret -n kube-system -o name | grep admin-user) | grep token
获取token后在dashboard的登录界面输入即可登录。

配置reset
当集群配置过程出现问题时中(特别是master),可以使用kubeadm reset命令将集群重置。
注意此命令比较危险,请不要随意尝试!


File: _posts/2019-10-06-jdk-stream-sa.md

[Binary file]


File: _posts/2020-01-08-rocketmq-dledger-01.md


layout: post title: "Dledger中Candidate选举流程" date: 2020-01-08 22:20:00 +0700 categories: [rocketmq,raft]

学习目标

搞清楚Dledger中Candidate的选举流程, 并且与Raft算法原本的步骤进行比较, 了解其中的不同之处以及原因

源码分析

流程的入口在DLedgerLeaderElector#maintainAsCandidate方法处:

java
if (System.currentTimeMillis() < nextTimeToRequestVote && !needIncreaseTermImmediately) {
    return;
}

首先判断nextTimeToRequestVote和needIncreaseTermImmediately(这两个值的设置在后文中有解释), 尚不需要请求投票,直接返回。

java
long term;
long ledgerEndTerm;
long ledgerEndIndex;

term: 请求投票时使用的term
ledgerEndTerm: 当前log的最新term
ledgerEndIndex:当前log的最新index

java
synchronized (memberState) {
    if (!memberState.isCandidate()) {
        return;
    }
    if (lastParseResult == VoteResponse.ParseResult.WAIT_TO_VOTE_NEXT || needIncreaseTermImmediately) {
        long prevTerm = memberState.currTerm();
        term = memberState.nextTerm();
        logger.info("{}_[INCREASE_TERM] from {} to {}", memberState.getSelfId(), prevTerm, term);
        lastParseResult = VoteResponse.ParseResult.WAIT_TO_REVOTE;
    } else {
        term = memberState.currTerm();
    }
    ledgerEndIndex = memberState.getLedgerEndIndex();
    ledgerEndTerm = memberState.getLedgerEndTerm();
}

这段代码的作用是计算出当前请求投票的term,当lastParseResult为WAIT_TO_VOTE_NEXT或者needIncreaseTermImmediately为true时,会将当前term自增1,否则为当前的term。
这里的lastParseResult和needIncreaseTermImmediately在后面有详细说明。

java
long startVoteTimeMs = System.currentTimeMillis();
final List<CompletableFuture<VoteResponse>> quorumVoteResponses = voteForQuorumResponses(term, ledgerEndTerm, ledgerEndIndex);

接着进入voteForQuorumResponses方法开始给集群内节点发送投票请求。

java
private List<CompletableFuture<VoteResponse>> voteForQuorumResponses(long term, long ledgerEndTerm,
    long ledgerEndIndex) throws Exception {
    List<CompletableFuture<VoteResponse>> responses = new ArrayList<>();
    for (String id : memberState.getPeerMap().keySet()) {
        VoteRequest voteRequest = new VoteRequest();
        voteRequest.setGroup(memberState.getGroup());
        voteRequest.setLedgerEndIndex(ledgerEndIndex);
        voteRequest.setLedgerEndTerm(ledgerEndTerm);
        voteRequest.setLeaderId(memberState.getSelfId());
        voteRequest.setTerm(term);
        voteRequest.setRemoteId(id);
        CompletableFuture<VoteResponse> voteResponse;
        if (memberState.getSelfId().equals(id)) {
            voteResponse = handleVote(voteRequest, true);
        } else {
            //async
            voteResponse = dLedgerRpcService.vote(voteRequest);
        }
        responses.add(voteResponse);

    }
    return responses;
}

可以看到,通过调用handleVote方法给自己投票,通过调用dLedgerRpcService.vote方法向远程节点请求。

处理投票请求

通过对比请求的term与自己的term的大小以及log的新旧来进行投票与否。
如果请求term小,返回REJECT_EXPIRED_VOTE_TERM。
如果term相等但是当前已经投票给某个节点,那么返回REJECT_ALREADY_VOTED。或者已经有正在生效的leader那么返回REJECT_ALREADY__HAS_LEADER。
如果自己的term比较小,那么先转为Candidate,并且将needIncreaseTermImmediately设置为true,返回REJECT_TERM_NOT_READY。设置needIncreaseTermImmediately为true,会影响到之后Candidate进行选举时计算term(见上面),而且可以想见计算的term一定不大于当前生效leader的term,而且极有可能在投票后会收到来自leader的心跳转为follower(见下方对nextTimeToRequestVote的分析)。
代码如下:

java
if (request.getTerm() < memberState.currTerm()) {
    return CompletableFuture.completedFuture(new VoteResponse(request).term(memberState.currTerm()).voteResult(VoteResponse.RESULT.REJECT_EXPIRED_VOTE_TERM));
} else if (request.getTerm() == memberState.currTerm()) {
    if (memberState.currVoteFor() == null) {
        //let it go
    } else if (memberState.currVoteFor().equals(request.getLeaderId())) {
        //repeat just let it go
    } else {
        if (memberState.getLeaderId() != null) {
            return CompletableFuture.completedFuture(new VoteResponse(request).term(memberState.currTerm()).voteResult(VoteResponse.RESULT.REJECT_ALREADY__HAS_LEADER));
        } else {
            return CompletableFuture.completedFuture(new VoteResponse(request).term(memberState.currTerm()).voteResult(VoteResponse.RESULT.REJECT_ALREADY_VOTED));
        }
    }
} else {
    //stepped down by larger term
    changeRoleToCandidate(request.getTerm());
    needIncreaseTermImmediately = true;
    //only can handleVote when the term is consistent
    return CompletableFuture.completedFuture(new VoteResponse(request).term(memberState.currTerm()).voteResult(VoteResponse.RESULT.REJECT_TERM_NOT_READY));
}

以上情况中只有term相等且当前没有投票给任何人才会进行下一步log的比较。
首先判断LedgerEndTerm,如果LedgerEndTerm相等则接下来判断LedgerEndIndex的是否是request比较大,如果是那么接受这个投票请求,否则拒绝。代码如下:

java
if (request.getLedgerEndTerm() < memberState.getLedgerEndTerm()) {
    return CompletableFuture.completedFuture(new VoteResponse(request).term(memberState.currTerm()).voteResult(VoteResponse.RESULT.REJECT_EXPIRED_LEDGER_TERM));
} else if (request.getLedgerEndTerm() == memberState.getLedgerEndTerm() && request.getLedgerEndIndex() < memberState.getLedgerEndIndex()) {
    return CompletableFuture.completedFuture(new VoteResponse(request).term(memberState.currTerm()).voteResult(VoteResponse.RESULT.REJECT_SMALL_LEDGER_END_INDEX));
}

if (request.getTerm() < memberState.getLedgerEndTerm()) {
    return CompletableFuture.completedFuture(new VoteResponse(request).term(memberState.getLedgerEndTerm()).voteResult(VoteResponse.RESULT.REJECT_TERM_SMALL_THAN_LEDGER));
}

memberState.setCurrVoteFor(request.getLeaderId());
return CompletableFuture.completedFuture(new VoteResponse(request).term(memberState.currTerm()).voteResult(VoteResponse.RESULT.ACCEPT);

对投票结果的处理

java
CountDownLatch voteLatch = new CountDownLatch(1);
        for (CompletableFuture<VoteResponse> future : quorumVoteResponses) {
    // ...
}

这里使用了CountDownLatch,当一些条件满足时释放该闭锁使流程前进,否则阻塞直到超时时间到达。

对每个投票结果的future,对以下几种投票结果进行处理:

java
public enum RESULT {
    UNKNOWN,
    ACCEPT, // 接受投票
    REJECT_UNKNOWN_LEADER, // 发起投票的机器不在集群中
    REJECT_UNEXPECTED_LEADER, // 
    REJECT_EXPIRED_VOTE_TERM, // 请求的term过期
    REJECT_ALREADY_VOTED, // 已投票给某个节点
    REJECT_ALREADY__HAS_LEADER, // 已经存在有效的leader,且该leader是其他机器
    REJECT_TERM_NOT_READY, // 本节点term过小(接下来转为Candidate)
    REJECT_TERM_SMALL_THAN_LEDGER, // 请求的term小于本机log的term
    REJECT_EXPIRED_LEDGER_TERM, // 请求log的term小于本机log的term
    REJECT_SMALL_LEDGER_END_INDEX;// 请求log的index小于本机log的index
}
  • 对于每一个ACCEPT,累加acceptedNum
  • 对于REJECT_ALREADY__HAS_LEADER,alreadyHasLeader设置为true
  • 对于REJECT_TERM_SMALL_THAN_LEDGER,REJECT_EXPIRED_VOTE_TERM,设置knownMaxTermInGroup(最大已知的term)
  • 对于REJECT_EXPIRED_LEDGER_TERM、REJECT_SMALL_LEDGER_END_INDEX,累加biggerLedgerNum
  • 对于REJECT_TERM_NOT_READY:累加notReadyTermNum
java
switch (x.getVoteResult()) {
    case ACCEPT:
        acceptedNum.incrementAndGet();
        break;
    case REJECT_ALREADY_VOTED:
        break;
    case REJECT_ALREADY__HAS_LEADER:
        alreadyHasLeader.compareAndSet(false, true);
        break;
    case REJECT_TERM_SMALL_THAN_LEDGER:
    case REJECT_EXPIRED_VOTE_TERM:
        if (x.getTerm() > knownMaxTermInGroup.get()) {
            knownMaxTermInGroup.set(x.getTerm());
        }
        break;
    case REJECT_EXPIRED_LEDGER_TERM:
    case REJECT_SMALL_LEDGER_END_INDEX:
        biggerLedgerNum.incrementAndGet();
        break;
    case REJECT_TERM_NOT_READY:
        notReadyTermNum.incrementAndGet();
        break;
    default:
        break;

}

这些变量在接下来Candidate的下一步操作中起到控制作用。

如果结果返回过程中出现以下情况:

  1. alreadyHasLeader为true,即集群中已经存在有效leader且term相等
  2. acceptedNum占半数以上
  3. acceptedNum + notReadyTermNum占半数以上

voteLatch立即countdown。

接下来开始解析投票结果, 如果存在已知更大的term那么就再次以该term进行选举:

java
if (knownMaxTermInGroup.get() > term) {
    parseResult = VoteResponse.ParseResult.WAIT_TO_VOTE_NEXT;
    nextTimeToRequestVote = getNextTimeToRequestVote();
    changeRoleToCandidate(knownMaxTermInGroup.get());
}

否则如果集群中已经有了一个同term的leader,那么继续投票(term增加),但是下一次投票的时间变长,
极有可能在间隔期间接收到来自leader的heartbeat.其中 heartBeatTimeIntervalMs 为一次心跳间隔时间, maxHeartBeatLeak 为 允许最大丢失的心跳

java
else if (alreadyHasLeader.get()) {
    parseResult = VoteResponse.ParseResult.WAIT_TO_VOTE_NEXT;
    nextTimeToRequestVote = getNextTimeToRequestVote() + heartBeatTimeIntervalMs * maxHeartBeatLeak;
}

否则如果在选举超时期间内收到的回复不到一半,那么进行重新投票(term不变)

java
else if (!memberState.isQuorum(validNum.get())) {
    parseResult = VoteResponse.ParseResult.WAIT_TO_REVOTE;
    nextTimeToRequestVote = getNextTimeToRequestVote();
}

如果收到的同意投票数超过一半,那么转变为leader

java
else if (memberState.isQuorum(acceptedNum.get())) {
    parseResult = VoteResponse.ParseResult.PASSED;
} 
if (parseResult == VoteResponse.ParseResult.PASSED) {
    logger.info("[{}] [VOTE_RESULT] has been elected to be the leader in term {}", memberState.getSelfId(), term);
    changeRoleToLeader(term);
}

否则如果此时同意投票的和NOT_READY(即term数小于请求term的节点会转变为Candidate)的超过半数,
那么立即进行下一轮投票(因为此时极有可能赢得选举)

java
else if (memberState.isQuorum(acceptedNum.get() + notReadyTermNum.get())) {
    parseResult = VoteResponse.ParseResult.REVOTE_IMMEDIATELY;
}

否则如果此时同意的和因为log过时拒绝的回复加在一起超过半数的话,那么重新投票

java
else if (memberState.isQuorum(acceptedNum.get() + biggerLedgerNum.get())) {
    parseResult = VoteResponse.ParseResult.WAIT_TO_REVOTE;
    nextTimeToRequestVote = getNextTimeToRequestVote();
}

其他情况:

java
else {
    parseResult = VoteResponse.ParseResult.WAIT_TO_VOTE_NEXT;
    nextTimeToRequestVote = getNextTimeToRequestVote();
}

转变为Leader

调用memberstate的changeToLeader方法转变为leader:

java
public synchronized void changeToLeader(long term) {
    PreConditions.check(currTerm == term, DLedgerResponseCode.ILLEGAL_MEMBER_STATE, "%d != %d", currTerm, term);
    this.role = LEADER;
    this.leaderId = selfId;
}

在接下来StateMaintainer的循环中就会根据当memberState的role进行leader的流程操作.

与原本的Raft算法的不同之处

  1. 当节点接收到一个term比自己大的Candidate节点A的vote请求时不会直接将票投给它,而是返回REJECT_TERM_NOT_READY,
    接着自己变成Candidate立即自增term发起投票,但是由于此时term必然不可能比原先被请求投票的那个节点A的大, 所以极有可能
    收到再次来自A的投票请求.

File: _posts/2020-02-17-rocketmq-dledger-02.md


layout: post title: "Dledger中Leader和Follower流程详解" date: 2020-02-17 15:20:00 +0700 categories: [rocketmq,raft]

学习目标

搞清楚Dledger中Leader和Follower的流程处理,重点关注心跳包的处理,因为心跳包是维持集群内一致性的重要手段之一,通过对心跳包的处理我们可以更加全面的了解Raft算法在数据不一致时的状态流转过程.

源码分析

首先来看Leader的状态维护流程

Leader

首先看maintainAsLeader方法:

java
private void maintainAsLeader() throws Exception {
        if (DLedgerUtils.elapsed(lastSendHeartBeatTime) > heartBeatTimeIntervalMs) {
            long term;
            String leaderId;
            synchronized (memberState) {
                if (!memberState.isLeader()) {
                    //stop sending
                    return;
                }
                term = memberState.currTerm();
                leaderId = memberState.getLeaderId();
                lastSendHeartBeatTime = System.currentTimeMillis();
            }
            sendHeartbeats(term, leaderId); // 发送心跳
        }
    }

代码很简单, 我们主要关注sendHeartbeats方法,下面是发送请求的部分代码:

java
for (String id : memberState.getPeerMap().keySet()) {
    if (memberState.getSelfId().equals(id)) {
        continue;
    }
    /**
    构造请求, 并且使用dLedgerRpcService发送请求.
    */
    HeartBeatRequest heartBeatRequest = new HeartBeatRequest();
    heartBeatRequest.setGroup(memberState.getGroup());
    heartBeatRequest.setLocalId(memberState.getSelfId());
    heartBeatRequest.setRemoteId(id);
    heartBeatRequest.setLeaderId(leaderId);
    heartBeatRequest.setTerm(term);
    CompletableFuture<HeartBeatResponse> future = dLedgerRpcService.heartBeat(heartBeatRequest);
    future.whenComplete((HeartBeatResponse x, Throwable ex) -> {
        try {

            if (ex != null) {
                throw ex;
            }
            switch (DLedgerResponseCode.valueOf(x.getCode())) {
                case SUCCESS:
                    succNum.incrementAndGet(); // 成功, 增加succNum
                    break;
                case EXPIRED_TERM:
                    maxTerm.set(x.getTerm()); // 存在更大的term
                    break;
                case INCONSISTENT_LEADER:
                    inconsistLeader.compareAndSet(false, true); // 不一致的leader
                    break;
                case TERM_NOT_READY:
                    notReadyNum.incrementAndGet(); // term比leader小
                    break;
                default:
                    break;
            }
            if (memberState.isQuorum(succNum.get())
                || memberState.isQuorum(succNum.get() + notReadyNum.get())) {
                beatLatch.countDown();
            }
        } catch (Throwable t) {
            logger.error("Parse heartbeat response failed", t);
        } finally {
            allNum.incrementAndGet();
            if (allNum.get() == memberState.peerSize()) {
                beatLatch.countDown();
            }
        }
    });
}
  1. 构造请求, 并且使用dLedgerRpcService发送请求.
  2. 根据请求的不同结果, 进行不同的计数器累加, 或者标识位的设置.
    1. SUCCESS, 增加succNum计数器
    2. EXPIRED_TERM, 说明存在更大的term,将该term设置到maxterm.
    3. INCONSISTENT_LEADER, 集群中存在另一个leader
    4. TERM_NOT_READY, term比leader小.

对心跳结果进行处理

java
if (memberState.isQuorum(succNum.get())) {
    lastSuccHeartBeatTime = System.currentTimeMillis();
} else {
    if (memberState.isQuorum(succNum.get() + notReadyNum.get())) {
        lastSendHeartBeatTime = -1;
    } else if (maxTerm.get() > term) {
        changeRoleToCandidate(maxTerm.get());
    } else if (inconsistLeader.get()) {
        changeRoleToCandidate(term);
    } else if (DLedgerUtils.elapsed(lastSuccHeartBeatTime) > maxHeartBeatLeak * heartBeatTimeIntervalMs) {
        changeRoleToCandidate(term);
    }
}
  1. 如果succNum占半数以上, 那么设置lastSuccHeartBeatTime, 返回, 这是正常情况.

    否则对异常情况进行判断.

  2. 如果term相同的 + term较小的占半数以上, 那么设置lastSendHeartBeatTime = -1, 即立即开始下一轮心跳包的发送.

  3. 如果存在更大的term或者集群中存在另一个leader, 那么转为candidate.

  4. 如果距离发送上一个心跳包的间隔时间超过了maxHeartBeatLeak * heartBeatTimeIntervalMs,那么此时转变为Candidate.(为什么要转为Candidate呢,在之后的follower篇,我们可以看到当follower超过maxHeartBeatLeak * heartBeatTimeIntervalMs没有收到心跳包时,会转为Candidate,此时大概率自己的term已经过期,所以转为Candidate较为合适).

Follower

我们首先来看Follower如何处理心跳包.

java
if (request.getTerm() < memberState.currTerm()) {
    return CompletableFuture.completedFuture(new HeartBeatResponse().term(memberState.currTerm()).code(DLedgerResponseCode.EXPIRED_TERM.getCode()));
} else if (request.getTerm() == memberState.currTerm()) {
    if (request.getLeaderId().equals(memberState.getLeaderId())) {
        lastLeaderHeartBeatTime = System.currentTimeMillis();
        return CompletableFuture.completedFuture(new HeartBeatResponse());
    }
}
  1. 如果leader的term小于自己的那么返回EXPIRED_TERM.

  2. 如果相等, 那么返回接收成功.

这里是一次不加锁的fast判断, 如果不满足这里的分支, 那么会进入下面的加锁逻辑中.

java
synchronized (memberState) {
    if (request.getTerm() < memberState.currTerm()) {
        return CompletableFuture.completedFuture(new HeartBeatResponse().term(memberState.currTerm()).code(DLedgerResponseCode.EXPIRED_TERM.getCode()));
    } else if (request.getTerm() == memberState.currTerm()) {
        if (memberState.getLeaderId() == null) {
            changeRoleToFollower(request.getTerm(), request.getLeaderId());
            return CompletableFuture.completedFuture(new HeartBeatResponse());
        } else if (request.getLeaderId().equals(memberState.getLeaderId())) {
            lastLeaderHeartBeatTime = System.currentTimeMillis();
            return CompletableFuture.completedFuture(new HeartBeatResponse());
        } else {
            //this should not happen, but if happened
            logger.error("[{}][BUG] currTerm {} has leader {}, but received leader {}", memberState.getSelfId(), memberState.currTerm(), memberState.getLeaderId(), request.getLeaderId());
            return CompletableFuture.completedFuture(new HeartBeatResponse().code(DLedgerResponseCode.INCONSISTENT_LEADER.getCode()));
        }
    } else {
        //To make it simple, for larger term, do not change to follower immediately
        //first change to candidate, and notify the state-maintainer thread
        changeRoleToCandidate(request.getTerm());
        needIncreaseTermImmediately = true;
        return CompletableFuture.completedFuture(new HeartBeatResponse().code(DLedgerResponseCode.TERM_NOT_READY.getCode()));
    }
}
  1. 如果leader的term小于自己的那么返回EXPIRED_TERM.

  2. 如果term相同, 那么判断leaderId, local的leaderId为null或者相同时, 会返回成功否则返回INCONSISTENT_LEADER.

  3. 如果local的term比较小, 那么此时返回TERM_NOT_READY, 不直接转为Follower,而是转为Candidate, 等待leader再次发送心跳包, 进入上面leaderId为null的逻辑, 转为follower.

maintainAsFollower

java
private void maintainAsFollower() {
    if (DLedgerUtils.elapsed(lastLeaderHeartBeatTime) > 2 * heartBeatTimeIntervalMs) {
        synchronized (memberState) {
            if (memberState.isFollower() && (DLedgerUtils.elapsed(lastLeaderHeartBeatTime) > maxHeartBeatLeak * heartBeatTimeIntervalMs)) {
                logger.info("[{}][HeartBeatTimeOut] lastLeaderHeartBeatTime: {} heartBeatTimeIntervalMs: {} lastLeader={}", memberState.getSelfId(), new Timestamp(lastLeaderHeartBeatTime), heartBeatTimeIntervalMs, memberState.getLeaderId());
                changeRoleToCandidate(memberState.currTerm());
            }
        }
    }
}

这里follower的逻辑非常简单, 判断距上次接收心跳包的时间是否超过maxHeartBeatLeak * heartBeatTimeIntervalMs, 如果超过, 那么判断leader不可用, 转为Candidate, 进行选举.


File: _posts/2020-02-18-rocketmq-dledger-03.md


layout: post title: "Dledger中日志追加流程概述" date: 2020-02-18 15:20:00 +0700 categories: [rocketmq,raft]

学习目标

搞清楚Dledger中Leader和Follower中对日志追加的流程.

源码分析

下面代码是leader处理客户端append请求.(DledgerServer#handleAppend)

java
long currTerm = memberState.currTerm();
if (dLedgerEntryPusher.isPendingFull(currTerm)) { //        1
    AppendEntryResponse appendEntryResponse = new AppendEntryResponse();
    appendEntryResponse.setGroup(memberState.getGroup());
    appendEntryResponse.setCode(DLedgerResponseCode.LEADER_PENDING_FULL.getCode());
    appendEntryResponse.setTerm(currTerm);
    appendEntryResponse.setLeaderId(memberState.getSelfId());
    return AppendFuture.newCompletedFuture(-1, appendEntryResponse);
} else {                                           //       2
    DLedgerEntry dLedgerEntry = new DLedgerEntry();
    dLedgerEntry.setBody(request.getBody());
    DLedgerEntry resEntry = dLedgerStore.appendAsLeader(dLedgerEntry);
    return dLedgerEntryPusher.waitAck(resEntry);
}

1: 由dLedgerEntryPusher判断当前term的pending请求是否大于阈值, 如果大于那么直接拒绝.
2: 如果未超过阈值, 那么首先构造dLedgerEntry并且存入leader的本地存储, 接着调用waitAck方法, 等待超过半数以上的follower接收日志返回.

appendAsLeader
存储结构大致与rocketmq的存储相同, 同样分为存储日志的部分和存储index的部分.在存储时需要构造这两个部分.

日志的结构如下:
magic - size - index - term - pos - channel - chain crc - body crc - body size - body
pos: 物理偏移量

index的结构如下:
magic - pos - size - index - term

只保留了关键代码

java
public DLedgerEntry appendAsLeader(DLedgerEntry entry) {
    ByteBuffer dataBuffer = localEntryBuffer.get();
    ByteBuffer indexBuffer = localIndexBuffer.get();
    DLedgerEntryCoder.encode(entry, dataBuffer);
    int entrySize = dataBuffer.remaining();
    synchronized (memberState) {
        long nextIndex = ledgerEndIndex + 1;
        entry.setIndex(nextIndex);
        entry.setTerm(memberState.currTerm());
        entry.setMagic(CURRENT_MAGIC);
        DLedgerEntryCoder.setIndexTerm(dataBuffer, nextIndex, memberState.currTerm(), CURRENT_MAGIC);
        long prePos = dataFileList.preAppend(dataBuffer.remaining()); // @1
        entry.setPos(prePos);
        DLedgerEntryCoder.setPos(dataBuffer, prePos);
        long dataPos = dataFileList.append(dataBuffer.array(), 0, dataBuffer.remaining()); // @2
        DLedgerEntryCoder.encodeIndex(dataPos, entrySize, CURRENT_MAGIC, nextIndex, memberState.currTerm(), indexBuffer);
        long indexPos = indexFileList.append(indexBuffer.array(), 0, indexBuffer.remaining(), false);
        ledgerEndIndex++;
        ledgerEndTerm = memberState.currTerm();
        if (ledgerBeginIndex == -1) {
            ledgerBeginIndex = ledgerEndIndex;
        }
        updateLedgerEndIndexAndTerm();
        return entry;
    }
}

这里的逻辑较为简单:

  1. 构造dataBuffer并且将日志条目的信息补充到buffer里去,然后将buffer写入到文件中.
  2. 构造indexBuffer, 将buffer也写入到文件中.
  3. 更新ledgerEndIndex和ledgerEndTerm,返回日志条目.

值得注意的细节:

java
long prePos = dataFileList.preAppend(dataBuffer.remaining());

此处调用dataFileList的preAppend方法主要是为了获取到日志条目的物理position.
dataBuffer的remaining获取到整个日志条目的大小.接着我们看preAppend方法.(省略非核心代码)

java
public long preAppend(int len, boolean useBlank) {
    MmapFile mappedFile = getLastMappedFile(); // 1
    if (null == mappedFile || mappedFile.isFull()) {
        mappedFile = getLastMappedFile(0);
    }
    int blank = useBlank ? MIN_BLANK_LEN : 0;
    if (len + blank > mappedFile.getFileSize() - mappedFile.getWrotePosition()) { // 2
        ByteBuffer byteBuffer = ByteBuffer.allocate(mappedFile.getFileSize() - mappedFile.getWrotePosition());
        byteBuffer.putInt(BLANK_MAGIC_CODE);
        byteBuffer.putInt(mappedFile.getFileSize() - mappedFile.getWrotePosition());
        if (mappedFile.appendMessage(byteBuffer.array())) {
            //need to set the wrote position
            mappedFile.setWrotePosition(mappedFile.getFileSize());
        } else {
            logger.error("Append blank error for {}", storePath);
            return -1;
        }
        mappedFile = getLastMappedFile(0);
        if (null == mappedFile) {
            logger.error("Create mapped file for {}", storePath);
            return -1;
        }
    }
    return mappedFile.getFileFromOffset() + mappedFile.getWrotePosition(); // 3

}
  1. 首先获取到最后一个MmapFile.(如果不存在,那么此处会进行创建)
  2. 接着判断MmapFile有没有足够空间写入日志条目, 如果没有, 那么会创建新的文件.
  3. 不论2是否创建新文件, 返回file当前的物理位移+file当前的wrotePosition作为要写入日志条目的起始物理位移.

让我们回到handleAppend方法里, leader自己写入日志条目之后就需要将日志推送到集群中到其他节点, 半数以上写入成功后commit, 才可以
返回客户端写入成功.

好的, 让我们开始分析waitAck方法:

java
public CompletableFuture<AppendEntryResponse> waitAck(DLedgerEntry entry) {
    updatePeerWaterMark(entry.getTerm(), memberState.getSelfId(), entry.getIndex()); // 1. 更新水位线
    if (memberState.getPeerMap().size() == 1) { // 2. 只有一个节点
        AppendEntryResponse response = new AppendEntryResponse();
        response.setGroup(memberState.getGroup());
        response.setLeaderId(memberState.getSelfId());
        response.setIndex(entry.getIndex());
        response.setTerm(entry.getTerm());
        response.setPos(entry.getPos());
        return AppendFuture.newCompletedFuture(entry.getPos(), response);
    } else {
        checkTermForPendingMap(entry.getTerm(), "waitAck");
        AppendFuture<AppendEntryResponse> future = new AppendFuture<>(dLedgerConfig.getMaxWaitAckTimeMs());
        future.setPos(entry.getPos());
        wakeUpDispatchers(); // 唤醒dispatcher, 方法日志
        return future;
    }
}
  1. 更新leader的水位线, 水位线关联的是一个MapMap<Long<* term *>, ConcurrentMap<String<* peerId *>, Long<* index *>>,用于记录集群内各个节点
    的日志情况.
  2. 如果只有一个节点, 那么直接返回即可, 如果不是, 那么调用wakeUpDispatchers, 唤醒dispatcher线程, 进行分发.

File: _posts/2020-02-18-rocketmq-dledger-04.md


layout: post title: "Dledger中日志追加流程详解" date: 2020-02-18 15:20:00 +0700 categories: [rocketmq,raft]

学习目标

搞清楚Dledger中日志传播的流程,重点分析EntryDispatcher、EntryHandler、QuorumAckChecker三个线程.

源码分析

在上一篇中讲到waitAck方法会等待超过半数以上的follower接收日志后返回客户端的append请求.
而对follower日志的分发则是通过DLedgerEntryPusher的dispatcher线程进行转发.

我们首先对DLedgerEntryPusher进行分析, 先看它的重要属性如下:

java
private Map<Long, ConcurrentMap<String, Long>> peerWaterMarksByTerm = new ConcurrentHashMap<>();
private Map<Long, ConcurrentMap<Long, TimeoutFuture<AppendEntryResponse>>> pendingAppendResponsesByTerm = new ConcurrentHashMap<>();

private EntryHandler entryHandler = new EntryHandler(logger);

private QuorumAckChecker quorumAckChecker = new QuorumAckChecker(logger);

private Map<String, EntryDispatcher> dispatcherMap = new HashMap<>();

peerWaterMarksByTerm: 每个节点基于term投票轮次的水位线标记.
pendingAppendResponsesByTerm: 每个日志index对应的appendFuture.
entryHandler: 线程, 可用于follower, 用来处理leader发来的日志.
quorumAckChecker: 线程, 用于leader, 用来处理日志处理的投票结果, 判断是否commit.
dispatcherMap: 线程, 用于leader, key是peerId, EntryDispatcher就是对应于每个节点的分发器.

EntryDispatcher

我们先查看它的doWork方法:

java
public void doWork() {
    try {
        if (!checkAndFreshState()) { // @1
            waitForRunning(1);
            return;
        }

        if (type.get() == PushEntryRequest.Type.APPEND) {
            doAppend(); // @2
        } else {
            doCompare(); // @3
        }
        waitForRunning(1);
    } catch (Throwable t) {
    }
}

checkAndFreshState方法, 只有返回true才会进行分发.

java
private boolean checkAndFreshState() {
    if (!memberState.isLeader()) { // @1
        return false;
    }
    if (term != memberState.currTerm() || leaderId == null || !leaderId.equals(memberState.getLeaderId())) { // @2
        synchronized (memberState) {
            if (!memberState.isLeader()) {
                return false;
            }
            PreConditions.check(memberState.getSelfId().equals(memberState.getLeaderId()), DLedgerResponseCode.UNKNOWN);
            term = memberState.currTerm();
            leaderId = memberState.getSelfId();
            changeState(-1, PushEntryRequest.Type.COMPARE);
        }
    }
    return true;
}

1: 判断当前是不是leader, 如果不是, 返回false
2: 如果是leader那么判断是否leader未设置(第一次进行分发)或者是leaderId不同(重新选举).如果是上述情况的某一种,那么会将dispatcher内的数学进行更新,并且转换为Compare模式.(转换为新的leader之后需要与follower进行日志比较确保没有不一致的日志产生)

changeState方法:

java
private synchronized void changeState(long index, PushEntryRequest.Type target) {
    switch (target) {
        case APPEND:
            compareIndex = -1;
            updatePeerWaterMark(term, peerId, index);
            quorumAckChecker.wakeup();
            writeIndex = index + 1;
            break;
        case COMPARE:
            if (this.type.compareAndSet(PushEntryRequest.Type.APPEND, PushEntryRequest.Type.COMPARE)) {
                compareIndex = -1;
                pendingMap.clear();
            }
            break;
        case TRUNCATE:
            compareIndex = -1;
            break;
        default:
            break;
    }
    type.set(target);
}

doAppend方法

java
private void doAppend() throws Exception {
    while (true) {
        if (!checkAndFreshState()) { // @1
            break;
        }
        if (type.get() != PushEntryRequest.Type.APPEND) { // @2
            break;
        }
        if (writeIndex > dLedgerStore.getLedgerEndIndex()) { // @3
            doCommit();
            doCheckAppendResponse();
            break;
        }
        if (pendingMap.size() >= maxPendingSize || (DLedgerUtils.elapsed(lastCheckLeakTimeMs) > 1000)) { // @4
            long peerWaterMark = getPeerWaterMark(term, peerId);
            for (Long index : pendingMap.keySet()) {
                if (index < peerWaterMark) {
                    pendingMap.remove(index);
                }
            }
            lastCheckLeakTimeMs = System.currentTimeMillis();
        }
        if (pendingMap.size() >= maxPendingSize) { // @5
            doCheckAppendResponse();
            break;
        }
        doAppendInner(writeIndex); // @6
        writeIndex++;
    }
}
  1. 检查当前状态是否是leader, 如果不是直接退出
  2. 检查是不是APPEND模式, 如果不是直接退出
  3. 判断writeIndex是否超出ledgerEndIndex, 如果超过,那么发送commit请求,并且尝试发送超时的append请求.出现此情况的原因是pendingMap(指的是Pusher的Map不是Dispatcher的)超过阈值(1w),此时会阻止追加, 在这个时候会出现dispatcher分发的速度超过leader的pending请求处理的速度)
  4. 如果此时该节点的pending请求(已经发送还未收到响应,但此时可能存在过期的记录)超过一个阈值(1000)或者距离上一次检查pendingMap泄漏的时间超过了1s,那么会进行pendingMap的清理,具体逻辑是根据当前peer的水位线清除之前的pendingMap记录.
  5. 如果此时pendingMap仍然大于阈值(此时是真正的没有收到响应的记录数), 那么此时会尝试重新发送最后一个请求.
  6. 发送新的append请求
java
private void doAppendInner(long index) throws Exception {
    DLedgerEntry entry = dLedgerStore.get(index);
    PreConditions.check(entry != null, DLedgerResponseCode.UNKNOWN, "writeIndex=%d", index);
    checkQuotaAndWait(entry); // @1
    PushEntryRequest request = buildPushRequest(entry, PushEntryRequest.Type.APPEND); // @2
    CompletableFuture<PushEntryResponse> responseFuture = dLedgerRpcService.push(request); // @3
    pendingMap.put(index, System.currentTimeMillis()); // @4
    responseFuture.whenComplete((x, ex) -> {
        try {
            PreConditions.check(ex == null, DLedgerResponseCode.UNKNOWN);
            DLedgerResponseCode responseCode = DLedgerResponseCode.valueOf(x.getCode());
            switch (responseCode) {
                case SUCCESS: // @5
                    pendingMap.remove(x.getIndex());
                    updatePeerWaterMark(x.getTerm(), peerId, x.getIndex());
                    quorumAckChecker.wakeup();
                    break;
                case INCONSISTENT_STATE: // @6
                    logger.info("[Push-{}]Get INCONSISTENT_STATE when push index={} term={}", peerId, x.getIndex(), x.getTerm());
                    changeState(-1, PushEntryRequest.Type.COMPARE);
                    break;
                default:
                    logger.warn("[Push-{}]Get error response code {} {}", peerId, responseCode, x.baseInfo());
                    break;
            }
        } catch (Throwable t) {
            logger.error("", t);
        }
    });
    lastPushCommitTimeMs = System.currentTimeMillis();
}

1 检查配额

  • 首先触发条件:append 挂起请求数已超过最大允许挂起数;基于文件存储并主从差异超过300m,可通过 peerPushThrottlePoint 配置。
  • 每秒追加的日志超过 20m(可通过 peerPushQuota 配置),则会 sleep 1s中后再追加。

2 构造append请求

java
private PushEntryRequest buildPushRequest(DLedgerEntry entry, PushEntryRequest.Type target) {
    PushEntryRequest request = new PushEntryRequest();
    request.setGroup(memberState.getGroup());
    request.setRemoteId(peerId);
    request.setLeaderId(leaderId);
    request.setTerm(term);
    request.setEntry(entry);
    request.setType(target);
    request.setCommitIndex(dLedgerStore.getCommittedIndex());
    return request;
}

3 使用dLedgerRpcService发送append请求

4 将当前发送日志的index和当前时间记录进pendingMap中.

5 对future的结果进行处理, 如果SUCCESS, 那么移除pendingMap中的记录, 并且更新水位线记录, 唤醒quorumAckChecker线程.

6 如果失败, 那么转换状态为COMPARE, 进行日志的比较重发.

关于follower如何处理append请求, 在之后的EntryHandler中会解释.

doCompare

java
while (true) {
    if (!checkAndFreshState()) {
        break;
    }
    if (type.get() != PushEntryRequest.Type.COMPARE
        && type.get() != PushEntryRequest.Type.TRUNCATE) {
        break;
    }
    if (compareIndex == -1 && dLedgerStore.getLedgerEndIndex() == -1) {
        break;
    }
    // 对compareIndex进行校验修改
    if (compareIndex == -1) {
        compareIndex = dLedgerStore.getLedgerEndIndex();
    } else if (compareIndex > dLedgerStore.getLedgerEndIndex() || compareIndex < dLedgerStore.getLedgerBeginIndex()) {
        compareIndex = dLedgerStore.getLedgerEndIndex();
    }

    DLedgerEntry entry = dLedgerStore.get(compareIndex); // @1
    PushEntryRequest request = buildPushRequest(entry, PushEntryRequest.Type.COMPARE);
    CompletableFuture<PushEntryResponse> responseFuture = dLedgerRpcService.push(request); // @2
    PushEntryResponse response = responseFuture.get(3, TimeUnit.SECONDS); // @3
    long truncateIndex = -1;

    if (response.getCode() == DLedgerResponseCode.SUCCESS.getCode()) { 
        if (compareIndex == response.getEndIndex()) { // @4
            changeState(compareIndex, PushEntryRequest.Type.APPEND);
            break;
        } else { // @5
            truncateIndex = compareIndex;
        }
    } else if (response.getEndIndex() < dLedgerStore.getLedgerBeginIndex()
        || response.getBeginIndex() > dLedgerStore.getLedgerEndIndex()) { // @6
        truncateIndex = dLedgerStore.getLedgerBeginIndex();
    } else if (compareIndex < response.getBeginIndex()) { // @7
        truncateIndex = dLedgerStore.getLedgerBeginIndex();
    } else if (compareIndex > response.getEndIndex()) { // @8
        compareIndex = response.getEndIndex();
    } else { // @9
        compareIndex--;
    }
    if (compareIndex < dLedgerStore.getLedgerBeginIndex()) { // @10
        truncateIndex = dLedgerStore.getLedgerBeginIndex();
    }
    if (truncateIndex != -1) {
        changeState(truncateIndex, PushEntryRequest.Type.TRUNCATE);
        doTruncate(truncateIndex);
        break;
    }
}

1 和 2 :构造compare请求, 发送

3 阻塞获取结果

4 如果返回结果为SUCCESS棒球compareIndex与follower的endIndex相同, 那么转为APPEND.返回结果为SUCCESS说明发送的日志与follower的日志比对成功, 说明只有compareIndex之后的日志需要重发,但是compareIndex与follower的endIndex相同意味着只需要进行APPEND就可以了.

5 如果compareIndex与follower的endIndex不同,那么意味着follower存在脏数据,所以需要进行truncate, leader会将truncateIndex(不包含)之后的日志发送给follower.

6 如果follower的endIndex和beginIndex不在leader的日志范围之内,那么将会发送leader所有的日志,设置truncateIndex为leader的beginIndex.

7 和 8 : 如果compareIndex小于follower的beginIndex,那么说明follower存在着磁盘故障(7很少发生), 如果compareIndex大于follower的endIndex, 那么需要从follwer的endIndex开始比较.

9 这种情况是比较常见的情况,此时compareIndex需要自减, 因为当前比较的结果失败, 需要找到最后一个leader和follower相同的日志Index, 如果找到了, 同时意味着之前所有的日志也都一致, 这是raft算法保证的.

10 如果truncateIndex不为-1,说明需要进行truncate,转为truncate状态,开始进行truncate.

doTruncate
源码如下:

java
private void doTruncate(long truncateIndex) throws Exception {
    DLedgerEntry truncateEntry = dLedgerStore.get(truncateIndex);
    PushEntryRequest truncateRequest = buildPushRequest(truncateEntry, PushEntryRequest.Type.TRUNCATE);
    PushEntryResponse truncateResponse = dLedgerRpcService.push(truncateRequest).get(3, TimeUnit.SECONDS);
    lastPushCommitTimeMs = System.currentTimeMillis();
    changeState(truncateIndex, PushEntryRequest.Type.APPEND);
}

这里没什么好说的,根据truncateIndex构造请求发送即可,发送之后就可以进入APPEND状态.

如上,就是Dispatcher的逻辑, 下面进入与follower关系密切的EntryHandler的代码中, 找到follower上面对APPEND、COMPARE、TRUNCATE等的处理.

EntryHandler

重要属性:

  • ConcurrentMap<Long, Pair>> writeRequestMap: 用来根据index存放对应的请求PushEntryRequest和存放结果的future.
  • BlockingQueue>> compareOrTruncateRequests: 阻塞队列,存放compare和truncate请求PushEntryRequest和对应的future.

主流程doWork:

java
public void doWork() {
    try {
        if (!memberState.isFollower()) {
            waitForRunning(1);
            return;
        }
        if (compareOrTruncateRequests.peek() != null) { // @1
            Pair<PushEntryRequest, CompletableFuture<PushEntryResponse>> pair = compareOrTruncateRequests.poll();
            PreConditions.check(pair != null, DLedgerResponseCode.UNKNOWN);
            switch (pair.getKey().getType()) { 
                case TRUNCATE:// @2
                    handleDoTruncate(pair.getKey().getEntry().getIndex(), pair.getKey(), pair.getValue());
                    break;
                case COMPARE: // @3
                    handleDoCompare(pair.getKey().getEntry().getIndex(), pair.getKey(), pair.getValue());
                    break;
                case COMMIT: // @4
                    handleDoCommit(pair.getKey().getCommitIndex(), pair.getKey(), pair.getValue());
                    break;
                default:
                    break;
            }
        } else {
            long nextIndex = dLedgerStore.getLedgerEndIndex() + 1; // @5
            Pair<PushEntryRequest, CompletableFuture<PushEntryResponse>> pair = writeRequestMap.remove(nextIndex);
            if (pair == null) {
                checkAbnormalFuture(dLedgerStore.getLedgerEndIndex()); // @6
                waitForRunning(1);
                return;
            }
            PushEntryRequest request = pair.getKey();
            handleDoAppend(nextIndex, request, pair.getValue()); // @7
        }
    } catch (Throwable t) {
        DLedgerEntryPusher.logger.error("Error in {}", getName(), t);
        DLedgerUtils.sleep(100);
    }
}

1: 如果阻塞队列中存在元素, 那么取出, 根据请求类型分别进行处理.(TRUNCATE和COMPARE和COMMIT)

2: 对TRUNCATE请求进行处理

3: 对COMPARE请求进行处理

4: 对COMMIT请求进行处理

5: 如果阻塞队列中没有元素, 那么查看writeRequestMap中是否存在本地日志下一个要写入的index的日志.

6: 如果不存在, 那么检查是否是leader推送的日志丢失了, 如果是那么需要leader重新发送COMPARE.

7: 处理APPEND请求

我们比较好奇writeRequestMap和compareOrTruncateRequests的数据是从哪里来的呢.答案在EntryHandler的handlePush方法中, 调用路径为:
DLedgerServer#handlePush -> DLedgerEntryPusher#handlePush -> EntryHandler#handlePush

让我们分别看一下上述 2 3 4的源码逻辑.

java
private CompletableFuture<PushEntryResponse> handleDoTruncate(long truncateIndex, PushEntryRequest request,
    CompletableFuture<PushEntryResponse> future) {
    try {
        long index = dLedgerStore.truncate(request.getEntry(), request.getTerm(), request.getLeaderId());
        PreConditions.check(index == truncateIndex, DLedgerResponseCode.INCONSISTENT_STATE);
        future.complete(buildResponse(request, DLedgerResponseCode.SUCCESS.getCode()));
        dLedgerStore.updateCommittedIndex(request.getTerm(), request.getCommitIndex());
    } catch (Throwable t) {
        logger.error("[HandleDoTruncate] truncateIndex={}", truncateIndex, t);
        future.complete(buildResponse(request, DLedgerResponseCode.INCONSISTENT_STATE.getCode()));
    }
    return future;
}

truncate逻辑很简单, 根据truncateIndex调用dLedgerStore的truncate方法,进行日志的truncate.

下面看COMPARE的逻辑.

java
private CompletableFuture<PushEntryResponse> handleDoCompare(long compareIndex, PushEntryRequest request,
    CompletableFuture<PushEntryResponse> future) {
    try {
        DLedgerEntry local = dLedgerStore.get(compareIndex);
        PreConditions.check(request.getEntry().equals(local), DLedgerResponseCode.INCONSISTENT_STATE);
        future.complete(buildResponse(request, DLedgerResponseCode.SUCCESS.getCode()));
    } catch (Throwable t) {
        logger.error("[HandleDoCompare] compareIndex={}", compareIndex, t);
        future.complete(buildResponse(request, DLedgerResponseCode.INCONSISTENT_STATE.getCode()));
    }
    return future;
}

根据compareIdex取到本地的日志entry与leader发送的entry做比较,如果相同那么返回SUCCESS, 否则返回INCONSISTENT_STATE.
这里和我们之前对dispatcher的分析吻合.

COMMIT的逻辑:

java
private CompletableFuture<PushEntryResponse> handleDoCommit(long committedIndex, PushEntryRequest request,
    CompletableFuture<PushEntryResponse> future) {
    try {
        dLedgerStore.updateCommittedIndex(request.getTerm(), committedIndex);
        future.complete(buildResponse(request, DLedgerResponseCode.SUCCESS.getCode()));
    } catch (Throwable t) {
        logger.error("[HandleDoCommit] committedIndex={}", request.getCommitIndex(), t);
        future.complete(buildResponse(request, DLedgerResponseCode.UNKNOWN.getCode()));
    }
    return future;
}

逻辑是否简单,仅仅是更新dLedgerStore的commitedIndex.

APPEND的逻辑如下:

java
private void handleDoAppend(long writeIndex, PushEntryRequest request,
    CompletableFuture<PushEntryResponse> future) {
    try {
        DLedgerEntry entry = dLedgerStore.appendAsFollower(request.getEntry(), request.getTerm(), request.getLeaderId());
        future.complete(buildResponse(request, DLedgerResponseCode.SUCCESS.getCode()));
        dLedgerStore.updateCommittedIndex(request.getTerm(), request.getCommitIndex());
    } catch (Throwable t) {
        logger.error("[HandleDoWrite] writeIndex={}", writeIndex, t);
        future.complete(buildResponse(request, DLedgerResponseCode.INCONSISTENT_STATE.getCode()));
    }
}

使用dLedgerStore进行日志的存储,接着尝试更新leader传来的commitIndex.
主要关注appendAsFollower方法, 但是这里对具体存储的部分我们不进行展开, 具体的我们下篇再详细讲解appendAsFollower的逻辑.

最后,我们分析最为复杂的doWork的代码@6部分,也就是checkAbnormalFuture方法.

java
private void checkAbnormalFuture(long endIndex) {
    if (DLedgerUtils.elapsed(lastCheckFastForwardTimeMs) < 1000) {
        return;
    }
    lastCheckFastForwardTimeMs  = System.currentTimeMillis();
    if (writeRequestMap.isEmpty()) {
        return;
    }
    long minFastForwardIndex = Long.MAX_VALUE;
    for (Pair<PushEntryRequest, CompletableFuture<PushEntryResponse>> pair : writeRequestMap.values()) {
        long index = pair.getKey().getEntry().getIndex();
        //Fall behind
        if (index <= endIndex) {
            try {
                DLedgerEntry local = dLedgerStore.get(index);
                PreConditions.check(pair.getKey().getEntry().equals(local), DLedgerResponseCode.INCONSISTENT_STATE);
                pair.getValue().complete(buildResponse(pair.getKey(), DLedgerResponseCode.SUCCESS.getCode()));
                logger.warn("[PushFallBehind]The leader pushed an entry index={} smaller than current ledgerEndIndex={}, maybe the last ack is missed", index, endIndex);
            } catch (Throwable t) {
                logger.error("[PushFallBehind]The leader pushed an entry index={} smaller than current ledgerEndIndex={}, maybe the last ack is missed", index, endIndex, t);
                pair.getValue().complete(buildResponse(pair.getKey(), DLedgerResponseCode.INCONSISTENT_STATE.getCode()));
            }
            writeRequestMap.remove(index);
            continue;
        }
        //Just OK
        if (index ==  endIndex + 1) {
            //The next entry is coming, just return
            return;
        }
        //Fast forward
        TimeoutFuture<PushEntryResponse> future  = (TimeoutFuture<PushEntryResponse>) pair.getValue();
        if (!future.isTimeOut()) {
            continue;
        }
        if (index < minFastForwardIndex) {
            minFastForwardIndex = index;
        }
    }
    if (minFastForwardIndex == Long.MAX_VALUE) {
        return;
    }
    Pair<PushEntryRequest, CompletableFuture<PushEntryResponse>> pair = writeRequestMap.get(minFastForwardIndex);
    if (pair == null) {
        return;
    }
    logger.warn("[PushFastForward] ledgerEndIndex={} entryIndex={}", endIndex, minFastForwardIndex);
    pair.getValue().complete(buildResponse(pair.getKey(), DLedgerResponseCode.INCONSISTENT_STATE.getCode()));
}

遍历writeRequestMap中待写入的日志,分别对以下情况进行处理:

  1. 如果index小于等于follower的endIndex, 那么说明该日志应该在follow而本地有存储, 比对它们之间的entry是否相同,如果不同那么返回INCONSISTENT_STATE,
    让leader发送COMPARE.

  2. 如果index正好等于endIndex+1, 那么意味着要写入的日志进入队列中,退出循环.

  3. 否则就是index > endIndex + 1的场合, 去判断该index对应的future是否已经超时, 如果已经超时那么记录该index到minFastForwardIndex中.

  4. 循环之外,说明此处endIndex+1的日志仍未到达, 如果记录了minFastForwardIndex, 那么判断其是否在仍然在待写入队列中, 如果仍然存在
    那么返回INCONSISTENT_STATE,要求leader重新比对日志.

上面就是follower接收到leader请求的动作, 下面我们看leader对follwer的返回结果是如何处理的.

QuorumAckChecker

代码比较长, 我们分几块来看.

java
if (pendingAppendResponsesByTerm.size() > 1) {
    for (Long term : pendingAppendResponsesByTerm.keySet()) {
        if (term == currTerm) {
            continue;
        }
        for (Map.Entry<Long, TimeoutFuture<AppendEntryResponse>> futureEntry : pendingAppendResponsesByTerm.get(term).entrySet()) {
            AppendEntryResponse response = new AppendEntryResponse();
            response.setGroup(memberState.getGroup());
            response.setIndex(futureEntry.getKey());
            response.setCode(DLedgerResponseCode.TERM_CHANGED.getCode());
            response.setLeaderId(memberState.getLeaderId());
            logger.info("[TermChange] Will clear the pending response index={} for term changed from {} to {}", futureEntry.getKey(), term, currTerm);
            futureEntry.getValue().complete(response);
        }
        pendingAppendResponsesByTerm.remove(term);
    }
}
if (peerWaterMarksByTerm.size() > 1) {
    for (Long term : peerWaterMarksByTerm.keySet()) {
        if (term == currTerm) {
            continue;
        }
        logger.info("[TermChange] Will clear the watermarks for term changed from {} to {}", term, currTerm);
        peerWaterMarksByTerm.remove(term);
    }
}

清理pendingAppendResponsesByTerm和peerWaterMarksByTerm.

java
Map<String, Long> peerWaterMarks = peerWaterMarksByTerm.get(currTerm);

long quorumIndex = -1;
for (Long index : peerWaterMarks.values()) {
    int num = 0;
    for (Long another : peerWaterMarks.values()) {
        if (another >= index) {
            num++;
        }
    }
    if (memberState.isQuorum(num) && index > quorumIndex) {
        quorumIndex = index;
    }
}

根据当前term找出当前占半数以上最大的index, 存入变量quorumIndex中.

java
if (quorumIndex >= 0) {
    for (Long i = quorumIndex; i >= 0; i--) {
        try {
            CompletableFuture<AppendEntryResponse> future = responses.remove(i);
            if (future == null) {
                needCheck = lastQuorumIndex != -1 && lastQuorumIndex != quorumIndex && i != lastQuorumIndex;
                break;
            } else if (!future.isDone()) {
                AppendEntryResponse response = new AppendEntryResponse();
                response.setGroup(memberState.getGroup());
                response.setTerm(currTerm);
                response.setIndex(i);
                response.setLeaderId(memberState.getSelfId());
                response.setPos(((AppendFuture) future).getPos());
                future.complete(response);
            }
            ackNum++; // 本次完成确认提交的日志数
        } catch (Throwable t) {
            logger.error("Error in ack to index={} term={}", i, currTerm, t);
        }
    }
}

从quorumIndex, 递减处理, 判断index对应的客户端future是否完成, 如果未完成, 那么构造reponse并且调用CompletedFuture的complete方法.
如果递减直到future以完成, 那么获取needChech的值,
needCheck为true,需要满足下面几个条件:

  1. 最后仲裁的日志号不为-1
  2. 最后仲裁的日志号不等于quorumIndex
  3. 当前日志号不等于最后仲裁的日志号.

needcheck是为了判断responses这个Map是否发生了泄漏存在的,观察needChechk为true的条件,
可以发现对于条件1: 即这次仲裁是第一次仲裁, 所以不存在泄漏的future, 为什么呢, 因为如果存在的话, 那么此时future==null如何解释?因为本次term第一次执行本方法,所以没有任何机会清理这个Map.
对于条件2: 即本次没有发生任何新增的需要确认的日志, 那么也就不存在泄漏的future.
对于条件3: 当前日志号等于lastQuorumIndex就意味着不存在泄漏的日志, 因为在上一次执行方法的时候就清理完了lastQuorumIndex之前的所有future.

java
if (ackNum == 0) {
    for (long i = quorumIndex + 1; i < Integer.MAX_VALUE; i++) {
        TimeoutFuture<AppendEntryResponse> future = responses.get(i);
        if (future == null) {
            break;
        } else if (future.isTimeOut()) {
            AppendEntryResponse response = new AppendEntryResponse();
            response.setGroup(memberState.getGroup());
            response.setCode(DLedgerResponseCode.WAIT_QUORUM_ACK_TIMEOUT.getCode());
            response.setTerm(currTerm);
            response.setIndex(i);
            response.setLeaderId(memberState.getSelfId());
            future.complete(response);
        } else {
            break;
        }
    }
    waitForRunning(1);
}

如果本次确认的日志数为0, 那么尝试从quorumIndex开始判断是否存在客户端的append的future是否超时,如果超时,
那么设置结果.

java
if (DLedgerUtils.elapsed(lastCheckLeakTimeMs) > 1000 || needCheck) {
    updatePeerWaterMark(currTerm, memberState.getSelfId(), dLedgerStore.getLedgerEndIndex());
    for (Map.Entry<Long, TimeoutFuture<AppendEntryResponse>> futureEntry : responses.entrySet()) {
        if (futureEntry.getKey() < quorumIndex) {
            AppendEntryResponse response = new AppendEntryResponse();
            response.setGroup(memberState.getGroup());
            response.setTerm(currTerm);
            response.setIndex(futureEntry.getKey());
            response.setLeaderId(memberState.getSelfId());
            response.setPos(((AppendFuture) futureEntry.getValue()).getPos());
            futureEntry.getValue().complete(response);
            responses.remove(futureEntry.getKey());
        }
    }
    lastCheckLeakTimeMs = System.currentTimeMillis();
}
lastQuorumIndex = quorumIndex;

检查responses是否产生泄漏, 即判断index是否小于当前确认的日志index即quorumIndex, 如果小于, 那么直接完成对应的future.


File: _posts/2020-06-07-zookeeper-leader-elect.md


layout: post title: "Zookeeper Leader选举流程源码解读" date: 2020-06-01 15:20:00 +0700 categories: [zookeeper]

学习目标

搞清楚Zookeeper是如何实现ZAB协议的领导者选举的.

源码分析

废话不多说, 让我进入源码分析的环节.
发起选举的节点状态为LOOKING状态,我们看org.apache.zookeeper.server.quorum.QuorumPeer#run方法:

java
while (running) {
                switch (getPeerState()) {
                case LOOKING:
                    LOG.info("LOOKING");

                    if (Boolean.getBoolean("readonlymode.enabled")) {...} else {
                        try {
                           reconfigFlagClear();
                           ...
                            setCurrentVote(makeLEStrategy().lookForLeader());
                        } catch (Exception e) {
                            LOG.warn("Unexpected exception", e);
                            setPeerState(ServerState.LOOKING);
                        }                        
                    }
                    break;
                case FLOWING: ...
                case ...

这里我们只关注LOOKING的case,其中注意makeLEStrategy().lookForLeader(),
makeLEStrategy方法选择了一种领导选举算法的实现,lookForLeader是真正进行选举的代码,这里makeLEStrategy
方法返回的就是org.apache.zookeeper.server.quorum.FastLeaderElection, 接下来让我们直接分析FastLeaderElection
的lookForLeader方法吧.

java
HashMap<Long, Vote> recvset = new HashMap<Long, Vote>();
HashMap<Long, Vote> outofelection = new HashMap<Long, Vote>();
int notTimeout = finalizeWait;
synchronized(this){
    logicalclock.incrementAndGet();
    updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
}

sendNotifications();

首先创建了两个Map, recvset和outofelection.

这里logicalclock自增1,然后更新自己的选票,这里logicalclock的初始值是-1, 用来表示当前的epoch,
但是接下来将会被自己的epoch更新.

getInitId返回myid, getInitLastLoggedZxid返回当前dataTree中最大的zxid, getPeerEpoch返回节点当前epoch.
这里其实是将选票投给自己.然后通过sendNotifications方法将vote发给集群内部所有节点.

java
for (long sid : self.getCurrentAndNextConfigVoters()) {
    QuorumVerifier qv = self.getQuorumVerifier();
    ToSend notmsg = new ToSend(ToSend.mType.notification, // type
            proposedLeader, // 投给谁? 这里是myid
            proposedZxid, // zxid
            logicalclock.get(), // 本地投票的epoch
            QuorumPeer.ServerState.LOOKING,
            sid, // 对面的id
            proposedEpoch, // 当前投的leader的epoch
            qv.toString().getBytes());
    sendqueue.offer(notmsg); // 放入发送队列中, 另外一个WorkSender线程会去发送
}

sendNotifications方法会将投票信息广播,当然也会发给自己,对于发给自己的投票直接将其放入RecvQueue中,
同时对于其他节点发回来的响应也放在RecvQueue中,等待lookForLeadder取.
在lookForLeadder的下一步就是不断从RecvQueue中取出响应,进行处理:

java
while ((self.getPeerState() == ServerState.LOOKING) && (!stop)){
    Notification n = recvqueue.poll(notTimeout, TimeUnit.MILLISECONDS);
    if (validVoter(n.sid) && validVoter(n.leader)) {
        switch (n.state) {
            case LOOKING:
            case FOLLOWING AND LEADING:
        }
    }
}

可以看到分别对当前发送投票的节点当前的状态做了处理,当其他节点正在选举和已经确定了Leader两种情况,
我们首先看其他节点也正在选举的情况.

我们对这种情况分两步来看,第一步对一个投票进行处理

java
if (n.electionEpoch > logicalclock.get()) {
    logicalclock.set(n.electionEpoch); // 设置新的投票轮次
    recvset.clear(); // 当前进行的投票过时了, 将recvset清除
    if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
            getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) {
        updateProposal(n.leader, n.zxid, n.peerEpoch);
    } else {
        updateProposal(getInitId(),
                getInitLastLoggedZxid(),
                getPeerEpoch());
    }
    sendNotifications();
} else if (n.electionEpoch < logicalclock.get()) {
    if(LOG.isDebugEnabled()){
        LOG.debug("Notification election epoch is smaller than logicalclock. n.electionEpoch = 0x"
                + Long.toHexString(n.electionEpoch)
                + ", logicalclock=0x" + Long.toHexString(logicalclock.get()));
    }
    break; // break switch
} else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
        proposedLeader, proposedZxid, proposedEpoch)) {
    updateProposal(n.leader, n.zxid, n.peerEpoch);
    sendNotifications();
}
recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));

首先明确下投票规则:

  1. 判断epoch, epoch大的获胜
  2. epoch相同的情况, zxid大的获胜
  3. 以上两个都相同的情况, myid大的获胜

再看上面的代码, 首先判断投票节点的投票时钟和自己的大小, 如果投票节点获胜, 那么再进行votePK,
如果投票节点获胜那么更新自己的选票并且将新的vote发送出去.
对于投票时钟相同的情况, 进行votePK, 如果投票胜出那么更新自己的投票,发送新的投票到各个节点.

代码如下:

java
protected boolean totalOrderPredicate(long newId, long newZxid, long newEpoch, long curId, long curZxid, long curEpoch) {
      return ((newEpoch > curEpoch) ||
                ((newEpoch == curEpoch) &&
                ((newZxid > curZxid) || ((newZxid == curZxid) && (newId > curId)))));
    }

最后将接收到的投票放入recvset中:recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch)).

每接收到投票, 都要判断一次当前轮次是否有人已经获胜, 代码如下:

java
if (termPredicate(recvset,
        new Vote(proposedLeader, proposedZxid,
                logicalclock.get(), proposedEpoch))) {
    // 此处在确定在finalizeWait时间内没有其他获胜者存在,
    // 因为zk在退出follower之后会提交所有没提交的txn, 所以不会
    // 像raft那样确保大多数节点具有最新提交的事务.
    while((n = recvqueue.poll(finalizeWait,
            TimeUnit.MILLISECONDS)) != null){
        if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
                proposedLeader, proposedZxid, proposedEpoch)){
            recvqueue.put(n);
            break;
        }
    }
    if (n == null) {
        self.setPeerState((proposedLeader == self.getId()) ?
                ServerState.LEADING: learningState());
        Vote endVote = new Vote(proposedLeader,
                proposedZxid, logicalclock.get(), 
                proposedEpoch);
        leaveInstance(endVote);
        return endVote;
    }
}

termPredicate方法判断当前节点选中的节点是否已经获胜(即集群中大多数的选票), 如果是那么将自己的状态
设置为LEADER或者FOLLOWER, 结束选举.

上面是从其他LOOKING节点上接收的选票,如果是LEADER或者是FOLLOWER状态的节点,那么会如何处理呢?

java
if(n.electionEpoch == logicalclock.get()){
    recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
    if(termPredicate(recvset, new Vote(n.version, n.leader,
                    n.zxid, n.electionEpoch, n.peerEpoch, n.state))
                    && checkLeader(outofelection, n.leader, n.electionEpoch)) {
        self.setPeerState((n.leader == self.getId()) ?
                ServerState.LEADING: learningState());
        Vote endVote = new Vote(n.leader, 
                n.zxid, n.electionEpoch, n.peerEpoch);
        leaveInstance(endVote);
        return endVote;
    }
}

outofelection.put(n.sid, new Vote(n.version, n.leader, 
        n.zxid, n.electionEpoch, n.peerEpoch, n.state));
if (termPredicate(outofelection, new Vote(n.version, n.leader,
        n.zxid, n.electionEpoch, n.peerEpoch, n.state))
        && checkLeader(outofelection, n.leader, n.electionEpoch)) {
    synchronized(this){
        logicalclock.set(n.electionEpoch);
        self.setPeerState((n.leader == self.getId()) ?
                ServerState.LEADING: learningState());
    }
    Vote endVote = new Vote(n.leader, n.zxid, 
            n.electionEpoch, n.peerEpoch);
    leaveInstance(endVote);
    return endVote;
}
  1. 当前LEADER与自己属于同一轮投票, 那么判断当recvset中该节点是否已经获得了大多数选票,
    如果是, 那么设置自己为LEADER或者FOLLOWING, 退出, 否则继续接收投票.

  2. 否则, 将投票放在outofelection中, 同样对outofelection进行LEADER有效判断, 如果有效
    那么设置状态返回.

这里的checkLeader方法可以看一下:

java
protected boolean checkLeader(
            Map<Long, Vote> votes,
            long leader,
            long electionEpoch){

        boolean predicate = true;

        if(leader != self.getId()){
            if(votes.get(leader) == null) predicate = false;
            else if(votes.get(leader).getState() != ServerState.LEADING) predicate = false;
        } else if(logicalclock.get() != electionEpoch) {
            predicate = false;
        }

        return predicate;
    }

我们要确定一个LEADER是否合法有以下要点:

  1. 该LEADER必须获取了足够的选票
  2. 该LEADER必须是存活的

第一点已经被保证(termPredicate), 那么只需要确认第二点.
如果别人认为我是LEADER,说明其他节点已经达成共识,我是LEADER,那么第二点自动满足.
如果认为别人是LEADER,那么需要防止该LEADER获取足够多选票之后宕机, 但是它的选票已经被
大多数节点持有并且不断发送, 就会陷入一种一台宕掉的节点获胜的情况, 为了防止这一点, 对有没有从
这个LEADER上拿到投票这一点做了判断并且是自认为是LEADER之后的vote.

总结

下面是流程图:


File: _posts/2020-06-09-rocketmq-messageconsume.md

[Binary file]


File: _posts/2020-06-13-zookeeper-datasync.md


layout: post title: "Zookeeper Leader成员发现以及数据同步流程源码分析" date: 2020-06-13 15:20:00 +0700 categories: [zookeeper]

学习目标

当Zookeeper完成了成员选举之后, 接下来就是要实现成员当发现了, 对于zk来说, 选举并没有实现一个集群成员当确定,
因为zk当选举没有回复, 只有通过集群节点内部不断的"举荐“过程才能知道自己是Leader, 所以选举之后
第一件事情就是确定集群内的成员,然后进行数据的一致性修复.

这里我们的学习目标就是搞清楚Zookeeper是如何实现ZAB协议的成员发现以及数据同步流程的.

成员发现

当选举成功之后, 该成为Leader的和Follower的都会进行各自的逻辑, 下面我们分别看看Leader和Follower会进行怎样的初始化流程.

Leader的初始化过程

我们首先来看Leader的初始化过程, 我们上面提到当完成了成员选举之后, 接下来就是要实现成员当发现了, 那么成员发现是怎么做到的呢?
我们带着问题来分析源码.

来看Leader.java的lead方法.

java
cnxAcceptor = new LearnerCnxAcceptor(); 
cnxAcceptor.start();

在这里新建了LearnerCnxAcceptor这个对象, 这个对象的作用是accept follower通过主从端口建立的连接请求,
并且初始化一个LeanerHandler的线程来处理follower上的请求以及心跳等, 这是我们接下来分析的重点.

java
long epoch = getEpochToPropose(self.getId(), self.getAcceptedEpoch());

这行代码的作用是确定acceptedEpoch, 什么是acceptedEpoch呢? 还有一个currentEpoch, 它们的区别是什么呢?
因为一个新当选的Leader需要将自己的epoch自增, 所以在这里需要将epoch+1, 那么为什么有两个变量呢?
这里其实牵扯到Zookeeper的一些历史遗留问题,有兴趣的可以看下这里的引用:

当一个集群内的zk节点重启时, 它的状态变化为LEADING-LOOKING—FOLLOWING,为什么呢?
是因为老版本的zk新节点重启后会接收到
其他节点的所有历史选举消息, 然而在原先的lookForLeader逻辑中,如果一个节点收到了来自集群内所有节点的消息时,它就会
退出LOOKING,变为LEADING,当然在这之后它会由于发送NEWLEADER消息没有ACK而回到FLE状态并且最终变为FOLOWER,但是在
退出LEADER的时候可能会将TnxLog写入到磁盘中,TxnLog的名称是epoch+zxid,在老版本的zk中,是没有acceptedEpoch的将epoch自增
是在转为LEADER之后,但是在连接到大多数节点之前,如果在上述的情况中,退出LEADER之前就将日志文件名称记录为最新的epoch中.
有人会问,记录到日志文件又怎么样?
实际上,zk会将磁盘中的TxnLog的文件名作为自己的epoch,这样的话此时这个重启之后的节点的epoch就比原先集群里面的大1,
此时转变为FOLLOWER时,由于在成员发现这一步(之后会讲到),发现LEADER的epoch竟然比自己小,那么自己又会退出FOLLOWER状态,重新进入FLE,
如此循环下去.
这是对应的issue:https://issues.apache.org/jira/browse/ZOOKEEPER-975 https://issues.apache.org/jira/browse/ZOOKEEPER-790
和wiki:https://cwiki.apache.org/confluence/display/ZOOKEEPER/Zab1.0#space-menu-link-content

  1. acceptedEpoch记录的是接收到最后的NEWEPOCH消息中的epoch.
  2. currentEpoch指接收到最新NEWLEADER消息中的epoch.

可以这么理解,一个是不稳定的新的epoch,一个是得到绝大多数节点公认的新的epoch.

java
public long getEpochToPropose(long sid, long lastAcceptedEpoch) throws InterruptedException, IOException {
    synchronized(connectingFollowers) {
        if (!waitingForNewEpoch) {
            return epoch;
        }
        if (lastAcceptedEpoch >= epoch) {
            epoch = lastAcceptedEpoch+1;
        }
        if (isParticipant(sid)) {
            connectingFollowers.add(sid);
        }
        QuorumVerifier verifier = self.getQuorumVerifier();
        if (connectingFollowers.contains(self.getId()) &&
                                        verifier.containsQuorum(connectingFollowers)) {
            waitingForNewEpoch = false;
            self.setAcceptedEpoch(epoch);
            connectingFollowers.notifyAll();
        } else {
            long start = Time.currentElapsedTime();
            long cur = start;
            long end = start + self.getInitLimit()*self.getTickTime();
            while(waitingForNewEpoch && cur < end) {
                connectingFollowers.wait(end - cur);
                cur = Time.currentElapsedTime();
            }
            if (waitingForNewEpoch) {
                throw new InterruptedException("Timeout while waiting for epoch from quorum");
            }
        }
        return epoch;
    }
}

这个方法会在Leader和LearnHandler两个地方调用,这个方法就是更新自己的acceptedEpoch,取Follower最大的+1,并且判断是否大多数的FOLLOWER已经给自己发送了FOLLOWERINFO消息,如果是那么唤醒所有在此方法上等待的线程,否则在此方法上等待.

这里的FOLLOWERINFO消息实际上就是FOLLOWER告诉LEADER自己当前的信息,就是成员发现的一个步骤.

FOLLOWER的初始化流程

接下来我们把目光转向FOLLOWER,看看FOLLOWER在选举之后进行的流程.

java
connectToLeader(leaderServer.addr, leaderServer.hostname); // 与LEADER建立连接
long newEpochZxid = registerWithLeader(Leader.FOLLOWERINFO); // 发送握手消息以及当前的acceptedEpoch,并且返回与LEADER达成一致的新的EPOCH

connectToLeader没什么好说的就是与LEADER在主从通信端口上建立socket连接.但是我们要注意LEADER在连接建立时候的行为,这个我们稍后再说.先看FOLLOWER的registerWithLeader方法做了什么事.

java
QuorumPacket qp = new QuorumPacket();                
qp.setType(pktType);
qp.setZxid(ZxidUtils.makeZxid(self.getAcceptedEpoch(), 0));
LearnerInfo li = new LearnerInfo(self.getId(), 0x10000, self.getQuorumVerifier().getVersion());
writePacket(qp, true);
readPacket(qp);

首先构建FOLLOWERINFO消息,这里用到了QuorumPacket,这是Zookeeper通用的原来通信的结构,主要包含如下部分:

java
private int type; // 指明消息类别
  private long zxid; // zxid
  private byte[] data; // 数据
  private java.util.List<org.apache.zookeeper.data.Id> authinfo; // 认证信息

将packet发送至LEADER之后等待响应.这时让我们把目光转向LEADER,看LEADER是如何处理FOLLOWER的连接以及FOLLOWERINFO消息的.

创建新的epoch

LearnerHandler在FOLLOWER与LEADER连接时被创建,是用来处理每个FOLLOWER的线程,下面我们来看它的run方法.

java
// .. 省略IO序列化相关代码
QuorumPacket qp = new QuorumPacket(); // @1
readRecord(qp, "packet");
byte learnerInfoData[] = qp.getData();
long lastAcceptedEpoch = ZxidUtils.getEpochFromZxid(qp.getZxid());
long zxid = qp.getZxid();
long newEpoch = leader.getEpochToPropose(this.getSid(), lastAcceptedEpoch);
long newLeaderZxid = ZxidUtils.makeZxid(newEpoch, 0);
QuorumPacket newEpochPacket = new QuorumPacket(Leader.LEADERINFO, newLeaderZxid, ver, null); // @2
oa.writeRecord(newEpochPacket, "packet");

ia.readRecord(ackEpochPacket, "packet");
ByteBuffer bbepoch = ByteBuffer.wrap(ackEpochPacket.getData());
ss = new StateSummary(bbepoch.getInt(), ackEpochPacket.getZxid());
leader.waitForEpochAck(this.getSid(), ss); // @3

1: 解析FOLLOWER传入的FOLLOWERINFO消息, 获取其lastAcceptedEpoch ,调用上面提到的getEpochToPropose方法,最终返回的是LEADER新的epoch,只不过这时还不能直接使用,还需要向FOLLOWER发送NEWEPOCH消息.
2: 构建LEADERINFO消息,将包含新的epoch的zxid发给FOLLOWER.
3: 等待FOLLOWER回复NEWEPOCH.

下面回到FOLLOWER,让我们看看FOLLOWER是如何处理NEWEPOCH消息的, 还是registerWithLeader方法

java
readPacket(qp);
final long newEpoch = ZxidUtils.getEpochFromZxid(qp.getZxid());
if (newEpoch > self.getAcceptedEpoch()) {
    wrappedEpochBytes.putInt((int)self.getCurrentEpoch());
    self.setAcceptedEpoch(newEpoch);
} else {
    throw new IOException("Leaders epoch, " + newEpoch + " is less than accepted epoch, " + self.getAcceptedEpoch());
}
QuorumPacket ackNewEpoch = new QuorumPacket(Leader.ACKEPOCH, lastLoggedZxid, epochBytes, null);
writePacket(ackNewEpoch, true);
return ZxidUtils.makeZxid(newEpoch, 0);

如果是LEADER比较大那么更新自己的AcceptedEpoch. 否则报错,进入FLE开始选举.
如果一切正常的话那么此时返回ACKEPOCH消息,代表自己接受LEADER的epoch,并且返回新的zxid.

接下里回到LEADER的线程中,我们看到Leader会阻塞在getEpochToPropose这个方法上,当所有节点都连接上之后Leader会这么样呢?

java
waitForEpochAck(self.getId(), leaderStateSummary);
self.setCurrentEpoch(epoch);

waitForNewLeaderAck(self.getId(), zk.getZxid());

首先回复自己的epochAck,然后当waitForEpochAck返回时也就代表着大多数节点已经接收了自己的epoch,那么下面就设置currentEpoch作为正式的epoch.(FOLLOWER什么时候设置currentEpoch呢?别急,接下里会看到)

然后leader开始等待newleader了,newLeader消息是在数据同步之后FOLLOWER才会ackLEADER的,所以这里我们先看数据同步的流程.

至此成员发现这一步算是结束了,接下来进入到数据同步的流程中.

数据同步

由于FOLLOWER在进入选举之前会将本地没有提交的事务全部提交(org.apache.zookeeper.server.quorum.Follower#shutdown),所以在新的LEADER当选的时候,一定存在某些日志缺失的,或者日志冲突的现象.这时候我们要将事务一致化,就需要LEADER将FOLLOWER的zxid(ACKEPOCH消息中)与自己的最新提交的zxid做比对,根据不同情况作出不同的处理.

我们定义以下几个变量:

java
maxCommittedLog: 提交的最大zxid
minCommittedLog: 内存中提交的最小zxid
lastProcessedZxid: dataTree上提交的最大zxid
peerLastZxid: follower的最大zxid,这个zxid是日志中的,可能是没有提交的.

其中maxCommittedLog和minCommittedLog是zk在内存中维护的一张链表,用于快速进行follower的日志同步.
有如下几种情况:

  1. lastProcessedZxid == peerLastZxid, 这种情况无需同步,因为dataTree中最新的zxid和follower的最新zxid相等,代表数据没有偏差
  2. peerLastZxid > maxCommittedLog, 说明follower上有脏数据这种情况需要进行TRUNC,truncate到maxCommittedLog即可
  3. minCommittedLog <= peerLastZxid <= maxCommittedLog,这种情况代表follower上缺少了一些事务,直接使用zk中的maxCommittedLog和minCommittedLog链表进行快速DIFF同步
  4. peerLastZxid < minCommittedLog, 代表follower落后的太多了,这时需要读取磁盘上的事务日志和内存中的链表进行DIFF同步或者采用SNAP方式进行同步

要注意,在进行Follower的sync处理过程中,leader可能已经进入原子广播阶段了(因为NEWLEADER的ACK只需要大多数节点响应即可进入下一步),所以在最后发送的同步日志中也包含一些已提交但还没有应用到Leader的dataTree上的一些数据.(见[[2020-08-02-zookeeper-request-handle#提案ACK消息的处理]])

数据同步--DIFF、TRUNC还是SNAP?

关键代码如下:

java
if (lastProcessedZxid == peerLastZxid) {
    queueOpPacket(Leader.DIFF, peerLastZxid);
} else if (peerLastZxid > maxCommittedLog && !isPeerNewEpochZxid) {
    // 对应单TRUNC,不会发送额外的packet
    queueOpPacket(Leader.TRUNC, maxCommittedLog);
    currentZxid = maxCommittedLog;
} else if ((maxCommittedLog >= peerLastZxid) && (minCommittedLog <= peerLastZxid)) {
    // 对应DIFF或者TRUNC+packets
    Iterator<Proposal> itr = db.getCommittedLog().iterator();
    currentZxid = queueCommittedProposals(itr, peerLastZxid,null, maxCommittedLog);
} else if (peerLastZxid < minCommittedLog && txnLogSyncEnabled) {
    // 对应DIFF/SNAP
    long sizeLimit = db.calculateTxnLogSizeLimit();
    Iterator<Proposal> txnLogItr = db.getProposalsFromTxnLog(
                        peerLastZxid, sizeLimit);
    if (txnLogItr.hasNext()) {
        currentZxid = queueCommittedProposals(txnLogItr, peerLastZxid,
                                                minCommittedLog, maxCommittedLog);

        Iterator<Proposal> committedLogItr = db.getCommittedLog().iterator();
        currentZxid = queueCommittedProposals(committedLogItr, currentZxid,
                                                null, maxCommittedLog);
    } else {
        snap = true;
    }
}
leaderLastZxid = leader.startForwarding(this, currentZxid);

startForwarding的作用是当这个follower是比较后来的,那么需要把当前toBeApplied(已被提交的但还没被应用到dataTree的proposal)
和outstandingProposals(还没被提交的proposal)的消息补发给这个follower.

对应上面的几种情况, 接下里我们对FOLLOWER端进行分析:

queueCommittedProposals代码:

java
long prevProposalZxid = -1;
long queuedZxid = peerLastZxid;
while (itr.hasNext()) {
    Proposal propose = itr.next();
    long packetZxid = propose.packet.getZxid();
    // abort if we hit the limit
    if ((maxZxid != null) && (packetZxid > maxZxid)) {
        break;
    }
        // 这个proposal follower已经有了,继续下一条
    if (packetZxid < peerLastZxid) {
        prevProposalZxid = packetZxid;
        continue;
    }

    if (needOpPacket) {
        // 说明之后掉事务都需要同步
        if (packetZxid == peerLastZxid) {
            queueOpPacket(Leader.DIFF, lastCommittedZxid);
            needOpPacket = false;
            continue;
        }

        if (isPeerNewEpochZxid) {
            queueOpPacket(Leader.DIFF, lastCommittedZxid);
            needOpPacket = false;
        } else if (packetZxid > peerLastZxid  ) {
            if (ZxidUtils.getEpochFromZxid(packetZxid) !=
                    ZxidUtils.getEpochFromZxid(peerLastZxid)) {
                // epoch不同,不能trunc https://issues.apache.org/jira/projects/ZOOKEEPER/issues/ZOOKEEPER-3607?filter=allopenissues
                LOG.warn("Cannot send TRUNC to peer sid: " + getSid() +
                            " peer zxid is from different epoch" );
                return queuedZxid;
            }
            // 代表这个提交follower 有,但是leader没有,从上一个大家都有的开始truncate
            queueOpPacket(Leader.TRUNC, prevProposalZxid);
            needOpPacket = false;
        }
    }
    // 发送proposal
    queuePacket(propose.packet);
    // 发送commit
    queueOpPacket(Leader.COMMIT, packetZxid);
    queuedZxid = packetZxid;
}

根据LEADER发送的不同请求类型:
DIFF: 接下来从leader那里会收到一系列的proposal和commit的packet.
TRUNC: 根据leader传来的maxCommittedLog, truncate掉从maxCommittedLog开始的所有日志
SNAP: 清空自己的zkDatabase, 反序列化leader传来的快照作为自己的zkDatabase

接下来就开始从leader接收proposal和commit包了.
将PROPOSAL放到packetsNotCommitted中临时存储, 等待匹配的COMMIT包, 将其应用到dataTree中.

同步完成之后收到NEWLEADER消息会返回ACK:

java
writePacket(new QuorumPacket(Leader.ACK, newLeaderZxid, null, null), true);

那么当Leader做完数据同步包的发送之后,节开始发送NEWLEADER:

java
QuorumPacket newLeaderQP = new QuorumPacket(Leader.NEWLEADER,
        newLeaderZxid, leader.self.getLastSeenQuorumVerifier()
                .toString().getBytes(), null);
queuedPackets.add(newLeaderQP);

leader.waitForNewLeaderAck(getSid(), qp.getZxid());

然后阻塞在waitForNewLeaderAck,等待大多数节点同步完成.

当大多数节点同步完成之后发送UPTODATE包,告诉FOLLOWER可以使用同步的数据了.

数据同步的收尾--进入原子广播阶段

接下来LearnerHandler开始根据FOLLOWER传来的包进行判断根据不同的类型进行不同的处理,开始进入原子广播阶段:

java
case Leader.ACK:
    leader.processAck(this.sid, qp.getZxid(), sock.getLocalSocketAddress());
case Leader.PING:
    leader.zk.touch(sess, to);
case Leader.REQUEST:
    leader.zk.submitLearnerRequest(si);

对于follower也是一样的,如下:

java
QuorumPacket qp = new QuorumPacket();
while (this.isRunning()) {
    readPacket(qp);
    processPacket(qp);
}

开始不断接收leader传来的proposal和commit请求.

在leader的主线程里,等待完成newLeader之后,开始准备接收客户端和follower的请求:

java
startZkServer();
// 判断心跳超时时间内是否与绝大多数follower保持连接

startZkServer开始建立requestProcessor链,它用来处理用户的请求,这个我们之后在原子广播中详述.

总结

下面是流程图:


File: _posts/2020-06-23-kafka-broker-request-process.md


layout: post title: "Kafka源码学习笔记 之 Broker端是如何处理请求的" date: 2020-07-13 15:20:00 +0700 categories: [kafka]

学习目标

学习kafka的broker端请求处理源码(源码位置:kafka/network/SocketServer.scala)

SocketServer的初始化

伪代码:

java
kafka.network.SocketServer#startup
createDataPlaneAcceptorsAndProcessors // 1
	for each endpoint:
		dataPlaneAcceptor = createAcceptor()
			new Acceoptor
		addDataPlaneProcessors(acceptor, processorNum) // 2
			new Processor // loop processorNum
			dataPlaneRequestChannel.addProcessor(processor) 
			acceptor.addProcessors // 3
startProcessingRequests // 4
	startDataPlaneProcessorsAndAcceptors
		acceptor.startProcessors
			for each processor.start()
  1. 启动DataPlane的Acceptor和Processor.
    实际上对每个Kafka监听的hostport,都会创建一个Acceptor线程,该线程接收入站连接,并将其分配给Processor.
  2. 创建好Acceptor后,为其创建processorNum个processor线程(这个变量由num.network.threads配置控制),同时将其加入到dataPlaneRequestChannel中.
  3. 创建好所有processors之后将其添加到Acceptor中.
  4. startProcessingRequests方法启动DataPlane的所有Acceptor和其Processor,实际上调用了acceptor的startProcessors方法,最终遍历调用processor的start方法.

连接的Accept

Acceptor类是一个线程,让我们看看它的run方法做了什么事.

java
serverChannel = openServerSocket(endPoint.host, endPoint.port) // 1
	serverChannel = ServerSocketChannel.open()
	serverChannel.socket.bind(socketAddress)
serverChannel.register(nioSelector, SelectionKey.OP_ACCEPT) // 2
while(isRunning)
	ready = nioSelector.select(500)
	if (ready > 0) {
		keys = nioSelector.selectedKeys() //3
		for key in keys is acceptable
		socketChannel = accept(key)
			socketChannel = serverSocketChannel.accept()
		assignNewConnection(socketChannel, processor) // 4
			processor.accept(socketChannel)
				newConnections.offer(socketChannel)
				~ async
				processor.configureNewConnections // 5
					channel = newConnections.poll()
					selector.register(connectionId(channel.socket), channel)
						registerChannel(id, socketChannel, SelectionKey.OP_READ);
					...
	}
  1. 这一步准确来说不是在run方法里面的,实际上是在Acceptor初始化的时候做的,不过不妨拿来一说.
    openServerSocket实际上使用NIO打开了一个ServerSocketChannel用来接收入站连接.
  2. 同样使用NIO的register方法注册感兴趣的事件为ACCEPT.
  3. 如果发生了ACCEPT事件,那么获取key,调用serverSocketChannel的accept方法获取SocketChannel.
  4. 接下来把得到的与client的连接,分配给acceptor拥有的其中一个processor线程,实际上调用processor的accept方法将连接塞到了processor的阻塞队列里.
  5. 那么什么时候processor从队列中取出呢?
    答案是在processor的configureNewConnections方法中,取出连接后注册READ事件,从channel中读取数据.

可以看到,Kafka的这个架构是一个非常经典的Reactor模式的应用.

Processor线程循环

processor同样是一个线程,我们来看它的一些重要属性:

scala
private val newConnections = new ArrayBlockingQueue[SocketChannel](connectionQueueSize)
private val inflightResponses = mutable.Map[String, RequestChannel.Response]()
private val responseQueue = new LinkedBlockingDeque[RequestChannel.Response]()
private val selector = createSelector(...)
  • newConnections是新接收的连接,不是processor当前处理的client的连接,这个要区分清楚.
  • inflightResponses是当前返回给client端的response,这个属性用于之后的response返回之后的回调.
  • responseQueue,是一个阻塞队列,I/O线程处理请求完成之后的响应会存放在这里
  • selector:对nio的selector的封装.

下面我们来看它的run方法:

scala
while (isRunning) {
  configureNewConnections() // 1
  processNewResponses() // 2
  poll() // 3
  processCompletedReceives() // 4
  processCompletedSends() // 5
  processDisconnected() // 6
  closeExcessConnections() // 7
}
  1. 处理新的客户端连接,实际上去注册READ事件.
  2. 从responseQueue中取出response,如果是SendResponse的话,调用selector的send方法,将responseSend返回给client.
  3. 底层调用selector的poll方法,针对不同的SelectionKey做不同处理.
    • 对read事件,读取channel中的请求数据将其放入到selecor的completedReceives属性中,将channel置为mute状态.(参见:[[2020-06-23-kafka-broker-request-process#细节 处理client端请求的有序性是如何保证的]])
    • 对write事件,调用channel的write方法,将response写出.
  4. 处理上一步中存放在selecor的completedReceives属性中的请求,调用requestChannel的sendRequest方法,将其放入到requestChannel的请求队列中.
  5. 这一步处理已经完成的发送响应,调用其回调方法,并且将client的channel置为unmute.
  6. 处理连接断开的channel
  7. 处理超出max.connections连接的情况,选择优先级最低的channel进行关闭.

细节:处理client端请求的有序性是如何保证的

RequestHandler


File: _posts/2020-06-23-kafka-study.md

[Binary file]


File: _posts/2020-07-27 mosn请求流程笔记.md


layout: post title: "mosn请求流程笔记" date: 2020-07-27 15:20:00 +0700 categories: [service mesh,mosn]

学习目标

学习mosn的请求处理流程

connPerGoroutine:

go
startRWLoop:
  connection.startReadLoop
    connection.doRead
	  c.readBuffer.ReadOnce(c.rawConnection)
	  c.onRead
	    c.filterManager.OnRead()

FilterManager的处理逻辑

go
FilterManager.onContinueReading
  activeReadFilter.filter.OnData(fm.conn.GetReadBuffer())

proxy/proxy.go的处理逻辑

go
proxy.serverStreamConn.Dispatch(buf)
  streamConn.Dispatch(buf)
  frame = streamConn.protocol.Decode(buf)
  // 如果读到了足够多的数据
  streamConn.handleFrame(streamCtx, frame)
    switch frame.GetStreamType() {
	case xprotocol.Request:
		sc.handleRequest(ctx, frame, false)
	case xprotocol.RequestOneWay:
		sc.handleRequest(ctx, frame, true)
	case xprotocol.Response:
		sc.handleResponse(ctx, frame)
	}

conn/streamConn.go
handleRequest方法处理逻辑

go
if frame.IsHeartbeatFrame() {
  // 处理心跳逻辑
}

serverStream := sc.newServerStream(ctx, frame)
proxy.NewStreamDetect
  downstream = newActiveStream // downstream是一个重要的东西
  downstream.OnReceive
    ~ downStream.receive

downstream的receive方法将downstream请求的处理分为如下几步:

  1. InitPhase:这一步基本什么也不做直接到下一步
  2. DownFilter:这一步执行downstream的receiverFilters(估计这个receiverFilters有大用处)
  3. MatchRoute:这一步根据请求头返回一个routeHandlerChain,然后开始执行这个chain,最终给downstream
    的route和snapshot赋值(snapshot是upstream集群的快照信息,里面包含集群的host集合和负载均衡器)
go
downstream.matchRoute()
  routersImpl.CallMakeHandlerChain
    DefaultMakeHandlerChain
	  routersImpl.MatchRoute(headers)
	    routersImpl.findVirtualHost(headers) // 根据virtualHost的domain配置可能返回默认的
  s.snapshot, s.route = handlerChain.DoNextHandler()
  1. DownFilterAfterRoute:路由之后再次执行downstream的receiverFilters.
  2. ChooseHost:因为已经确定上游集群了,接下来开始确定集群中的host.
go
downStream.chooseHost()
  s.cluster = s.snapshot.ClusterInfo()
  s.initializeUpstreamConnectionPool
    connPool = s.proxy.clusterManager.ConnPoolForCluster(s.snapshot, currentProtocol) // 这里会进行连接池的创建和初始化
	  clusterManager.getActiveConnectionPool // 这里会进行负载均衡
	    clusterSnapshot.LoadBalancer().ChooseHost(balancerContext)
  s.requestInfo.OnUpstreamHostSelected(pool.Host()) // 设置host
  1. DownFilterAfterChooseHost:选择host之后运行receiverFilter
  2. DownRecvHeader:大致意思是创建一个sender,这个sender包含一个StreamReceiveListener,用来将响应解码.代码路径如下:
go
downstream.receiveHeaders()
  downstream.upstreamRequest.appendHeaders()
  upstreamRequest.connPool.NewStream(downStream.context, r, r)
    streamEncoder = activeClient.client.NewStream(ctx, responseDecoder) // responseDecoder就是upstreamRequest
      streamSender := client.ClientStreamConnection.NewStream(context, receiver)
	    clientStream:=streamConn.newClientStream()
	    clientStream.receiver = receiver
		streamConn.clientStreams[clientStream.id] = clientStream // 方便响应到来时进行对应
	  receiver.stream = streamSender.GetStream()
    upstreamRequest.OnReady(streamEncoder, host)
  1. DownRecvData:请求的发送
go
downstream.receiveData()
  s.onUpstreamRequestSent() // 设置超时
  s.upstreamRequest.appendData(endStream)
    r.requestSender.AppendData(r.downStream.context, r.convertData(data), endStream)
      if endStream {
		  s.endStream() // 发送请求
		    buf, err := s.sc.protocol.Encode(s.ctx, s.frame)
			s.sc.netConn.Write(buf)
	  }
  1. WaitNotify:等待响应:downstream.waitNotify(id)

响应的接收:

go
streamConn.handleResponse
  clientStream, ok := sc.clientStreams[requestId]
  clientStream.receiver.OnReceive(clientStream.ctx, frame.GetHeader(), frame.GetData(), nil) // clientStream是xStream,receiver是StreamReceiveListener即upstreamRequest
    r.downStream.downstreamRespHeaders = headers
	r.downStream.downstreamRespDataBuf = data
	r.downStream.downstreamRespTrailers = trailers
	r.downStream.sendNotify() // 发送响应通知
  1. UpRecvData:接收响应数据
go
upstreamRequest.receiveData()
  downStream.onUpstreamData()
    downStream.onUpstreamResponseRecvFinished()
	  downStream.cleanUp()
	downStream.appendData(endStream)
	  data := s.convertData(s.downstreamRespDataBuf) // 当下游数据和上游数据当协议不一致时进行转换
	  xStream.AppendData(data types.IoBuffer)
	    xStream.endStream()
		  buf, err := s.sc.protocol.Encode(s.ctx, s.frame) // 给响应编码
		  err = s.sc.netConn.Write(buf) // 写出响应
		downStream.endStream()
		  downStream.cleanStream()

File: _posts/2020-08-02-zookeeper-request-handle.md


layout: post title: "Zookeeper请求处理全流程" date: 2020-08-02 17:20:00 +0700 categories: [zookeeper]

学习目标

对于Zk来说,除了Leader选举和成员发现、数据同步之外,更重要的是ZK在正常状态:原子广播模式下的运作,
所以这篇文章力求搞清楚Zk是如何处理客户端请求的,我们主要以setData为例.

客户端网络

客户端连接初始化

java
new Zookeeper() // @1
  ClientCnxnSocket getClientCnxnSocket()
    new ClientCnxnSocketNetty().connect()
	  bootstrap.connect() // @2
	    incomingBuffer = lenBuffer; // @3
		ClientCncx.primeConnection() // @4
		ZKClientPipelineFactory.initChannel() 
		  pipeline.addLast("handler", new ZKClientHandler()) // @5
  1. 整个过程是在创建客户端Zookeeper对象的时候进行的
  2. 使用我们熟知的netty的bootstrap创建连接
  3. 将incomingBuffer设置为lenBuffer,lenBuffer是专门用来读取rpc消息的长度字段的,
    而且这个incomingBuffer会在lenBuffer和真正的数据buffer之间来回切换
  4. 调用ClientCncx的primeConnection方法,创建server的session.
  5. ZKClientPipelineFactory会将ZKClientHandler加入netty的handler链中

Packet

java
requestHeader // xid type
request  // Record
replyHeader // xid zxid err
response // Record
callback
context

请求流程图

![[zookeeper客户端请求.png]]

SendThread

SendThread的Loop

java
if (!clientCnxnSocket.isConnected())
  clientCnxnSocket.connect(addr) // @1
clientCnxnSocket.doTransport(to, pendingQueue, ClientCnxn)
  Packet head = outgoingQueue.poll(waitTimeOut, TimeUnit.MILLISECONDS) // @2
  doWrite(pendingQueue, head, cnxn);
    while (true) {
	  p.requestHeader.setXid(cnxn.getXid()); // @3
	  synchronized (pendingQueue) { // @4
	    pendingQueue.add(p);
	  }
	  sendPktOnly(p); // 不flush @5
	  anyPacketsSent = true;
	  if (outgoingQueue.isEmpty()) // @6
	    break;
	}
	if (anyPacketsSent)
	  channel.flush(); // @7
  1. 如果socket没有连接到server,先建立连接
  2. 建立连接之后,从outgoingQueue中取出待发送到packet
  3. 如果成功取到,那么将取到到packet到requestHeader设置xid,xid是自增的
  4. 并且将packet加入到pendingQueue中,等待响应
  5. 将packet发送出去,但是不flush缓冲区
  6. 注意到这是个while(true)循环,所以会持续到outgoing全部发送完毕
  7. 如果存在有packet发送的话,那么会将缓冲区flush.

SendThread的readResponse

java
// 处理pings、AuthPacket、notification的响应... 略过,不是我们分析的重点
SendThread.readResponse() // @1
  packet = pendingQueue.remove(); // @2
  lastZxid = replyHdr.getZxid(); // @3
  SendThread.finishPacket() 
    if (p.cb == null) { // @4
	  p.notifyAll();
	} else { 
	  eventThread.queuePacket(p); // @5
	}
  1. SendThread.readResponse处理返回的server端响应
  2. 从pendingQueue中出队一个Packet(pendingQueue中存的是等待响应的Packet,见[[2020-08-02-zookeeper-request-handle#请求流程图]]),因为Zk的命令执行保证是FIFO的,所以响应也是按照顺序返回的.
  3. 更新lastZxid
  4. 如果是同步的情况,那么唤醒在此packet上等待的线程,见[[2020-08-02-zookeeper-request-handle#同步API--create]]
  5. 异步的情况,将packet放入eventThread中.

异步API--create

java
Zookeeper.create()
  ClientCnxn.queuePacket()
    packet = new Packet()
	outgoingQueue.add(packet);
	sendThread.getClientCnxnSocket().packetAdded();

可以看到,实际上就是把请求加入到ongoingQueue中

eventThread

在之前readResponse流程中,我们知道,对于异步请求来说,响应会调用eventThread的queuePacket方法:
[[2020-08-02-zookeeper-request-handle#SendThread的readResponse]]
实际上eventThread中有一个队列waitingEvents,里面会存放返回的响应Packet,所以queuePacket实际上将Packet入队.
然后eventThread本身也是个线程,我们看它的运行主逻辑:

java
while (true) {
  Object event = waitingEvents.take(); // @1
  processEvent(event); // @2
  ...
	  if (p.response instanceof Create2Response) {
		cb.processResult()
	  }
}
  1. 从waitingEvents出队
  2. 根据不同请求的响应进行callback的调用.

同步API--create

java
Zookeeper.create()
	ClientCnxn.submitRequest() // @1
	  Packet packet = queuePacket()  // @2
	  while (!packet.finished) {
		packet.wait(); // @3
	  }
  1. 调用ClientCnxn的submitRequest方法
  2. 将packet入队到ongoingQueue中(queuePacket见![[2020-08-02-zookeeper-request-handle#异步API--create]]),由SendThread进行发送.
  3. 无限等待,直到响应到达.

那么什么时候唤醒呢?

java
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf buf) throws Exception {
while (buf.isReadable()) {
	if (incomingBuffer.remaining() > buf.readableBytes()) {
		int newLimit = incomingBuffer.position()
				+ buf.readableBytes();
		incomingBuffer.limit(newLimit);
	}
	buf.readBytes(incomingBuffer); // @1
	incomingBuffer.limit(incomingBuffer.capacity());

	if (!incomingBuffer.hasRemaining()) {
		incomingBuffer.flip();
		if (incomingBuffer == lenBuffer) { // @2
			recvCount.getAndIncrement();
			readLength();
			  -> incomingBuffer = ByteBuffer.allocate(len);
		} else if (!initialized) { // @3
			readConnectResult();
			lenBuffer.clear();
			incomingBuffer = lenBuffer;
			initialized = true;
			updateLastHeard();
		} else {
			sendThread.readResponse(incomingBuffer); // @4
			lenBuffer.clear();
			incomingBuffer = lenBuffer;
			updateLastHeard();
		}
	}
}
wakeupCnxn();
}
  1. 一开始我们要读取消息的头四位即len到incomingBuffer中,确定接下来消息的长度,所以不会进入到上面的if中.
  2. 如果我们还在读取len的话,就调用readLength方法将incomingBuffer方法切换为读取消息体的buffer.
  3. 如果我们还在一开始的初始化阶段(初始化的时候会发送建立session的请求[[2020-08-02-zookeeper-request-handle#客户端连接初始化|]])
  4. 如果是正常的响应消息的话,会从incomingBuffer中读取响应,这里会调用sendThread的readResponse方法,见:[[2020-08-02-zookeeper-request-handle#SendThread的readResponse]],接下来清理lenBuffer,将incomingBuffer重新设置为lenBuffer.

服务端请求处理

网络请求处理

NettyServerCnxnFactory.java

java
void channelActive()
  NettyServerCnxn cnxn = new NettyServerCnxn()
  ctx.channel().attr(CONNECTION_ATTRIBUTE).set(cnxn); // @1

关于Netty如何建立服务端我们不再赘述,让我们直接看要点.

  1. 在netty的channelActive方法中代表一个连接的新建,这里每建立一个新的连接都会创建一个NettyServerCnxn,然后将它与channel关联起来,为后续的channelRead作准备

下面看channelRead:

java
void channelRead()
  NettyServerCnxn cnxn = ctx.channel().attr(CONNECTION_ATTRIBUTE).get(); // @1
  cnxn.processMessage((ByteBuf) msg); // @2
    NettyServerCnxn.receiveMessage(buf);
	  ZooKeeperServer.processPacket(buf);
	    ZooKeeperServer.submitRequest(Request);
		  firstProcessor.processRequest(Request); // @3
  1. 取出该channel关联的NettyServerCnxn
  2. 调用NettyServerCnxn的processMessage方法处理数据
  3. 最终由ZooKeeperServer中的firstProcessor进行请求的处理

那么firstProcessor是在那里配置的呢?
实际上对于Follower和Leader来说,Processor的配置是不一样的,但它们都是一个串起来的链所组成的形式.
下面就来研究一下这个Processor链的处理过程.

RequestProcessor处理链

请求在Zk中是要经过RequestProcessor处理链的,处理链在Follower和Leader上是不同的,而且处理读请求与写请求之间也有细微的差别.
关于RequestProcessor接口定义如下:

java
void processRequest(Request request);
void shutdown();

其中processRequest在RequestProcessor实现类中一般都是将请求放到实现类中的阻塞队列就OK了(除了FinalRequestProcessor和ToBeAppliedRequestProcessor).

由于Follower上的处理流程比较简单,我们首先来看写请求在Follower上的处理流程.

写请求在Follower上的处理流程

Follower上的RequestProcessor处理链为:
![[Follower处理链.png]]
其中SyncRequestProcessor主要处理来自Leader的Proposal,SendAck用来处理向Leader发送Proposal的响应.

正常的提交到follower的请求走的是上面这个链路.

Request表示的是在RequestProcessor链中移动的请求,让我们来看它有哪些重要的字段:

java
sessionId
cxid
type
TxnHeader hdr
zxid

我们首先来看FollowerRequestProcessor:

FollowerRequestProcessor

FollowerRequestProcessor的processRequest就是将请求放到队列中就返回了,我们主要看它的run方法(FollowerRequestProcessor本身是一个线程)

java
loop:
  Request request = queuedRequests.take(); // @1
  nextProcessor.processRequest(request); // @2
  switch (request.type) {
    case OpCode.create 
	...
	  zks.getFollower().request(request); // @3
  }
  1. 从内部的队列中取出一个请求
  2. 提交给下一个处理器进行处理,下一个处理器是Commit处理器,而Commit处理器的processRequest的实现也是提交到队列中,所以这一步是异步的.
  3. 如果是写请求,需要向Leader发送请求.

接下来就到了Commit处理器,Commit处理器可能是最复杂的处理器了,我会对其进行详解.

CommitProcessor

Zk为了保证最大化并发度但是又必须保证数据的一致性做出了以下约束:

  1. 读请求可以并发执行
  2. 读请求和写请求互斥
  3. 任何时候只能有一个写请求在处理
    我们可以在接下来的代码分析中看到zk是如何确保这几点的.

首先请看下面的图:
![[队列关系.png]]
可以看到在CommitProcessor中有两个队列:queuedRequests和committedRequests,两个Request容器:nextPending和currentlyCommitting.

  • queuedRequests的作用和之前的处理器一样,是为了异步执行解耦用的.
  • committedRequests用来存放由Leader发送的Commit请求,这里的Commit请求可能是别的follower的请求结果也可能就是当前Follower的.
  • nextPending存放的是正在等待Leader回复Commit消息的request,在nextPending存在期间,zk是不会处理任何queuedRequests中的请求的.
  • currentlyCommitting.代表正在执行FInalRequestProcssor的请求,同样当currentlyCommitting不为空的时候也不允许其他请求执行.

其流程大致如下:

  1. 首先由上一个处理器发来请求放到queuedRequests中
  2. CommitProcessor主线程判断当前是否可以执行请求,如果可以就从queue中拿出来请求.这里判断的标准是不能有任何正在等待提交或者等待处理已提交的请求(即判断nextPending和currentlyCommitting是否为空),如果存在,那么挂起执行线程直到其为空.
  3. 根据其是否是读请求,如果是读请求,直接进入下一个处理器,如果是写请求,则需要放到nextPending中等待FollowerRequestProcessor中提交给Leader的Proposed的commit响应[[2020-08-02-zookeeper-request-handle#FollowerRequestProcessor]]
  4. 由Leader返回的commit响应会放到committedRequests这个队列中,这一步会wakeup CommitProcessor的处理线程.注意这一步中的commit消息可能不是nextPending中等待的commit.
  5. 由committedRequests中取出commit消息与nextPending中等待的request尝试匹配,匹配的字段是cxid和sessionId,如果都相同则匹配否则不匹配
  6. 无论是否匹配,都会设置currentlyCommitting,如果匹配的话还需要清除nextPending
  7. 进入最后的Final处理器处理,主要是修改内存中的dataTree,这里可能会返回响应给客户端

FinalRequestProcessor

FinalRequestProcessor:最后一个processor,将事务应用到DataTree中同时把响应返回给客户端.
伪代码:

java
processRequest:
  zks.processTxn(request); // @1
    dataTree.processTxn(hdr, txn);
  if (request.isQuorum()) // @2
    zks.getZKDatabase().addCommittedProposal(request);
    zks.getZKDatabase().addCommittedProposal(request);
  rsp = new CreateResponse(); // @3
  cnxn.sendResponse(rsp)
  1. 将request应用到dataTree中.
  2. 如果是写请求则需要加入到ZkDataBase维护的一个列表,这是用来fast follower synchronization的,关于这一点待补充
  3. 最后将请求处理的响应发给客户端.

关于Follower接收客户端请求的流程我们已经说完了,下面我们来分析Follower的第二条处理链,Sync -> SendAck.
先来看SyncRequestProcessor:

SyncRequestProcessor

Follower从什么地方进入到SyncRequestProcessor的呢?
下面是进入的伪代码步骤:

java
Follower#processPacket
  case Leader.PROPOSAL:
    FollowerZooKeeperServer.logRequest(hdr, txn);
	  syncProcessor.processRequest(request);

可以看到,是在Follower接收到Leader发来的Proposal的时候才会进入Sync中.

我们来看SyncRequestProcessor的主要功能是什么:
负责把事务记录到磁盘中,在ZK里就是txnLog和snap,而且做了group commit的优化.
只有将request代表的事务记录到磁盘中,才会继续往下传递request.
下面是对SyncRequestProcessor源码上注释的翻译:

text
SyncRequestProcessor用于以下三个场景:
1. Leader: 将request写到磁盘,并且转发给AckRequestProcessor,AckRequestProcessor的作用是回复一个ack给自己.
2. Follower: 将request写到磁盘,并且转发给SendAckRequestProcessor,它将给Leader发送响应
3. Observer: 将已提交的请求刷至磁盘,它的下一个processor是null,也就说它不会给Leader发送响应.

SyncRequestProcessor接收请求:

java
public void processRequest(Request request) {
	// request.addRQRec(">sync");
	queuedRequests.add(request);
}

该方法只有一行,将请求加入到queuedRequests队列中.

SyncRequestProcessor主流程伪代码:

java
loop:
  si = queuedRequests.poll(); // @1
  if si == null // @2
    flush(toFlush)
  zks.getZKDatabase().append(si) // @3
  toFlush.add(si) // @4
  if (toFlush.size() > 1000)  // @5
    flush(toFlush);
  1. 从queuedRequests中尝试取出一个请求
  2. 如果当前没有请求,说明我们很闲😄,这时候去flush
  3. 这一步实际上调用txnLog的append方法,将其写入日志中.注意这一步仅仅是write,并没有flush.另外只有写请求会append,读请求不会.
  4. 将请求加入待flush列表中.
  5. 如果等待flush的请求大于1000,那么进行flush,这一步实际上会将所有待flush的请求向后面的processor转发.

SendAckRequestProcessor

这一步是Follower第二条链的最后一个处理器,看名字就能知道它的作用是向Leader发送确认消息.
实际上代码确实这么简单:

java
QuorumPacket qp = new QuorumPacket
  learner.writePacket

写请求在Leader上的处理流程

在[[2020-08-02-zookeeper-request-handle#FollowerRequestProcessor]]的处理流程中我们可以看到,Follower会把请求发给Leader,而Leader会在[[2020-06-13-zookeeper-datasync#数据同步的收尾--进入原子广播阶段]]这里接收Follower的请求,我们来看submitLearnerRequest的伪代码:

java
prepRequestProcessor.processRequest(request);

它只有一行代码,就是将我们的请求提交到了PrepRequestProcessor,在看PrepRequestProcessor之前,我们先把Leader的整个处理器链路给画出来.
![[Leader处理链.png]]

我们首先来看PrepRequestProcessor

PrepRequestProcessor

PrepRequestProcessor的processRequest方法,将请求放在内部的submittedRequests阻塞队列中.

java
public void processRequest(Request request) {
	submittedRequests.add(request);
}

它同时也是一个线程,它会从submittedRequests中取出请求,进行处理:

java
while (true) {
	Request request = submittedRequests.take();
	if (Request.requestOfDeath == request) { // "毒丸"设计模式
		break;
	}
	pRequest(request);
}
}

pRequest实际上根据request的type去做相应的处理,具体做什么处理呢,是将写请求加入到ZookeeperServer到outstandingChanges和outstandingChangesForPath属性中,我们举个例子来说吗这两个属性的作用:
当session关闭时,需要清理该会话下的所有临时节点,但是实际场景下,closeSession请求处理之前如果已经有下面两类请求到达服务器并且正在处理:

  • 节点(包含临时与非临时)删除请求,删除的目标节点正好是上述临时节点中的一个。
  • 临时节点创建,修改请求,目标节点正好是上述临时节点中的一个。

对于第一类,由于我们closeSession请求的目的就是删除临时节点,所以到closeSession请求处理时会造成重复删除,所以我们将closeSession请求待删除列表中移除该节点.
对于第二类,我们将临时节点创建,修改请求对应的节点路径加入到待删除列表中.

我们以create操作为例:

java
pRequest2Txn(request.type, zks.getNextZxid(), request, create2Request, true)
  request.setHdr(new TxnHeader(request.sessionId, request.cxid, zxid, Time.currentWallTime(), type)); // @1
  pRequest2TxnCreate(type, request, record, deserialize);
    int newCversion = parentRecord.stat.getCversion()+1; // @2
	request.setTxn(new CreateTxn(path, data, listACL, createMode.isEphemeral(),
                    newCversion)); // @3 ?
	parentRecord.childCount++; // @4
	parentRecord.stat.setCversion(newCversion); 
	addChangeRecord(parentRecord); // @5
	  zks.outstandingChanges.add(c);
	  zks.outstandingChangesForPath.put(c.path, c);
	addChangeRecord(new ChangeRecord(request.getHdr().getZxid(), path, s, 0, listACL)); // @6
  1. 设置事务头,包含sessionId、创建node时的zxid(即当前的zxid)、下一个zxid等
  2. 设置父节点等cversion自增
  3. 设置事务数据,包含本次创建等path和数据等
  4. 父节点等child数量自增同时更新cversion
  5. 将父节点改变记录加入到outstandingChanges中.
  6. 将节点改变记录加入到outstandingChanges中.

ProposalRequestProcessor

ProposalRequestProcessor的作用主要是对写请求会给follower发送提案获得大多数follower的ack后进行提交,它的代码也是直观的:

java
nextProcessor.processRequest(request);
if (request.getHdr() != null) {
	// We need to sync and get consensus on any transactions
	try {
		zks.getLeader().propose(request);
		  QuorumPacket pp = new QuorumPacket(Leader.PROPOSAL, request.zxid, data);
		  outstandingProposals.put(lastProposed, p);
		  sendPacket(pp);
	} catch (XidRolloverException e) {
		throw new RequestProcessorException(e.getMessage(), e);
	}
	syncProcessor.processRequest(request);
}

可以看到我们会首先将请求传递给下一个processor,而下一个processor实际上就是commitprocessor(见:[[2020-08-02-zookeeper-request-handle#CommitProcessor]]).
同时在Leader的propose方法中除了发送请求之外还会将提案加入到outstandingProposals以便和后续的commit请求对应起来.
对于写请求还会把它传递给SyncProcessor进行事务的持久化(见:[[2020-08-02-zookeeper-request-handle#SyncRequestProcessor]]).

AckRequestProcessor

AckRequestProcessor实际上就是Leader给自己的proposal返回ack的过程,代码过于简单就不详细说明了.

java
QuorumPeer self = leader.self;
if(self != null)
	leader.processAck(self.getId(), request.zxid, null);

提案ACK消息的处理

上面我们说到了在ProposalRequestProcessor中我们会发送Proposal给Follower,我们来看Follower是如何处理的.
使用伪代码描述流程如下:

java
Follower.followLeader
  while (this.isRunning()) {
	readPacket(qp);
	processPacket(qp);
	  case Leader.PROPOSAL:
	    // 反序列化...
		syncProcessor.processRequest(request);
  }

我们可以看到最后是由syncProcessor进行处理的,之前我们已经分析过Follower的第二条处理链的执行步骤了,这里就不赘述了,总之最后第二条链进入FInal处理器会发送Ack给Leader,让我们看Leader是在哪里处理的.

java
LearnerHandler.run
  ...
  case Leader.ACK: // @1
    leader.processAck
	  Proposal p = outstandingProposals.get(zxid); // @2
	  tryToCommit(p, zxid, followerAddr) 
	    if (p.hasAllQuorums()) // @3
		{
		  outstandingProposals.remove(zxid); // @3.1
		  toBeApplied.add(p); // @3.2
		  zk.commitProcessor.commit(p.request); // @3.3
		}
  1. 在LeanerHandler线程中,接收到Follower的消息,判断是ACK的话进入到processAck的方法中
  2. 在这里我们从outstandingProposals中尝试获取到我们在ProposalRequestProcessor中设置进去的提案[[2020-08-02-zookeeper-request-handle#ProposalRequestProcessor]]
  3. 如果获得了大多数Follower的确认的才进入下面的步骤,否则直接返回
    1. 从outstandingProposals移除提案
    2. 加入提案到toBeApplied,toBeApplied代表已经提交但还没有应用到dataTree中的提案
    3. 调用commitProcessor的commit方法,会与之前设置的nextPending做匹配,详细分析见:[[2020-08-02-zookeeper-request-handle#CommitProcessor]]

总结

以上就是zookeeper对于客户端请求的整体解读.
我们首先看了客户端上如何发送请求给zookeeper server的,接着又看了server端的处理流程,重点分析了Follower和Leader的请求处理器链以及Follower与Leader之间的交互,可以看到ZAB协议的原子广播在这里的应用.


File: _posts/2020-08-12-design-of-unix-buffer-cache-concepts.md

[Binary file]


File: _posts/2020-08-13-design-of-unix-filesystem-structure.md


layout: post title: "unix操作系统设计-第六章进程结构笔记" date: 2020-08-14 21:17:00 +0700 categories: [unix]

学习目标

学习如下内容
6.1 进程状态转换
6.2 进程的内存管理--虚拟内存到无力地址的转换是如何做的
6.3 进程上下文
6.4在中断的时候如何保存进程上下文和恢复
6.5一些系统调用
6.6唤醒与睡眠进程的算法

进程状态转换

进程转换图如下所示:
![[Pasted image 2.png]]
两个内核结构用来描述进程:

  1. 进程表项,由内核访问
  2. u区,由进程访问

进程表项含有如下字段:

  • state:表示进程的状态
  • 包含字段使内核可以定位进程和其u区.用来进行上下文切换的时候保存当前进程的信息
  • 包含event descript字段当进程处于sleep状态

u区包含的字段:

  • 进程表项的指针
  • 当前目录,当前根
  • 用户描述符表,记录打开的文件
  • 一些IO参数[[2020-08-13-design-of-unix-filesystem-systemcall#算法read步骤]]

File: _posts/2020-08-13-design-of-unix-filesystem-systemcall.md


layout: post title: "unix操作系统设计-第五章文件系统的系统调用笔记" date: 2020-08-13 21:33:00 +0700 categories: [unix]

学习目标

学习和文件系统相关的系统调用,包括open、read、write、lseek、close(访问现有文件),和create、mknod(创建新文件)和chdir、chroot、chown、chmod、stat、fstat(改变文件的inode)和一些其他的高级调用.

核心词汇

文件描述符表
文件表
内存索引节点表

概念解释

![[Pasted image.png]]
如图所示,文件描述符表存储着被进程打开的文件的标示符,其指向文件表,文件表中存储的是文件系统中每个被打开的文件的信息,其指向索引节点表,索引节点表存储的是每个文件的索引节点.

算法open步骤

fd=open(pathname,flags,modes)

  1. 首先通过namei获得上锁的索引节点
  2. 给file table分配表项,指向索引节点,初始化引用数和offset,默认是0(read和write开始的地方)
  3. 分配用户描述符表项,设置指向file table表项的指针
  4. inode解锁
  5. 返回fd

文件描述符的0、1、2分别是标准输入、标准输出、标准异常

算法read步骤

number=read(fd,buf,count)

  1. 从fd拿到file table表项
  2. 校验文件是否可访问
  3. 设置u区的IO参数
    ![[Pasted image 1.png]]
  4. 从file table中拿到inode
  5. 锁住inode
  6. loop:当读取的字节数未到count
    1. 使用bmap将offset转换为block中要读取多少数据,如果为0说明到达文件末尾返回
    2. 否则读取block中的数据到内核缓冲区(bread),再将数据拷贝至用户指定的buf
    3. 更新u区的参数
    4. 释放bread锁住的内核缓冲区
  7. 解锁inode
  8. 更新file table的offset,表示下次读取/写入从哪里开始
  9. 返回读取了多少字节

算法write步骤

write的过程中如果数据块不够,可能要动态分配(alloc算法),在动态分配的过程中,
还要修改inode的磁盘块记录.


File: _posts/2020-09-26-cpp-template-note.md


layout: post title: "c++模版笔记" date: 2020-09-26 22:09:00 +0700 categories: [c++]

基础

类模版声明与定义

header中的类声明示例如下:

cpp
template <typename T>
class Grid {
    ...
    Grid(const Grid& src) = default;
    Grid<T>& operator=(const Grid& rhs) = default;
    
    const std::optional<T>& at(size_t x, size_t y) const;
}

cpp中定义如下:

cpp
template <typename T>
const std::optional<T>& Grid<T>::at(size_t x, size_t y) const {
    ...
}

方法模版声明与定义

因为Grid与Grid不是一个类型,所以通常情况下它们之间无法相互赋值.
然而通过定义新的拷贝和赋值函数可以实现这一点.
声明方法:

cpp
template <typename T>
class Grid
{
    public:
        // Omitted for brevity

        template <typename E>
        Grid(const Grid<E>& src);

        template <typename E>
        Grid<T>& operator=(const Grid<E>& rhs);

        void swap(Grid& other) noexcept;

        // Omitted for brevity
};

我们在每个方法上都加了新的模版声明template <typename E>以此来代表与T是不同的类型.
另外一种声明方式:

cpp
template <typename T>
template <typename E>
Grid<T>::Grid(const Grid<E>& src)
    : Grid(src.getWidth(), src.getHeight())
{
    // The ctor-initializer of this constructor delegates first to the
    // non-copy constructor to allocate the proper amount of memory.

    // The next step is to copy the data.
    for (size_t i = 0; i < mWidth; i++) {
        for (size_t j = 0; j < mHeight; j++) {
            mCells[i][j] = src.at(i, j);
        }
    }
}

注意template <typename E>必须在template <typename T>的下面.
而且在这样的copy 函数和赋值函数中使用到的其他成员函数必须是public的.

copy和赋值函数定义如下:

cpp
template <typename T>
template <typename E>
Grid<T>& Grid<T>::operator=(const Grid<E>& rhs)
{
    // no need to check for self-assignment because this version of
    // assignment is never called when T and E are the same

    // Copy-and-swap idiom
    Grid<T> temp(rhs); // Do all the work in a temporary instance
    swap(temp); // Commit the work with only non-throwing operations
    return *this;
}

进阶

选择性初始化 (Selective Instantiation)

对于虚函数, 编译器会自动生成相关代码.
而对于非虚函数, 如果不显式调用, 编译器不会自动生成代码.

将template代码的声明与定义分开

通常我们只需要include一个class的header就可以调用它的函数了, 具体的定义在link时得到,
但是对于template class就不可以, 因为它要在编译时确定所有, 所以必须能够在编译时就将方法定义等
include进来, 但是为了保持良好的代码结构, 我们需要一种方法能够保证定义与声明分离.

使用这样一种方法, 将方法定义分离到另外一个header中,然后在声明的header里去include实现的header,
注意这个include需要放在文件的末尾.

cpp
template <typename T>
class Grid
{
    // Class definition omitted for brevity
}
#include "GridDefinitions.h"

template 参数

template定义中可以不仅仅包含type参数, 也可以包含其他参数, 比如如下的定义:

cpp
template <typename T, size_t MAX_WIDTH = 10>
class Grid {
    ...
}

这里甚至可以给出一个默认值.
注意Grid<T, 2>和Grid<T, 3>不是一个类型,不可相互赋值.而且在初始化模版的时候,
不能使用变量初始化比如Grid<T, width>是非法的.

template根据构造器进行类型推断

原先可能要这样写:

cpp
std::pair<int, double> pair1(1, 2.3);

现在只要这样写:

cpp
std::pair pair3(1, 2.3);

template特别定制款

可以为某一个class专门定制实现, 如下:

cpp
// 必须包含原template class声明
#include "Grid.h"

// 必须要有!
template <>
class Grid<const char*>
{
    public:
        explicit Grid(size_t width = kDefaultWidth,
            size_t height = kDefaultHeight);
        virtual ~Grid() = default;

        // Explicitly default a copy constructor and assignment operator.
        Grid(const Grid& src) = default;
        Grid<const char*>& operator=(const Grid& rhs) = default;

        // Explicitly default a move constructor and assignment operator.
        Grid(Grid&& src) = default;
        Grid<const char*>& operator=(Grid&& rhs) = default;

        std::optional<std::string>& at(size_t x, size_t y);
        const std::optional<std::string>& at(size_t x, size_t y) const;

        size_t getHeight() const { return mHeight; }
        size_t getWidth() const { return mWidth; }

        static const size_t kDefaultWidth = 10;
        static const size_t kDefaultHeight = 10;

    private:
        void verifyCoordinate(size_t x, size_t y) const;

        std::vector<std::vector<std::optional<std::string>>> mCells;
        size_t mWidth, mHeight;
};

可以看到这种定制款可以完全和template class完全不一样.

继承模版类

和继承普通类一样, 继承模版类的声明也是相当自然的.

cpp
#include "Grid.h"

template <typename T>
class GameBoard : public Grid<T>
{
    public:
        explicit GameBoard(size_t width = Grid<T>::kDefaultWidth,
            size_t height = Grid<T>::kDefaultHeight);
        void move(size_t xSrc, size_t ySrc, size_t xDest, size_t yDest);
};

GameBoard继承了Grid的所有方法.

需要注意:使用泛型继承时,子类使用父类的方法时必须在方法前指定父类的名字:如Grid::

cpp
template <typename T>
GameBoard<T>::GameBoard(size_t width, size_t height)
    : Grid<T>(width, height)
{
}

template <typename T>
void GameBoard<T>::move(size_t xSrc, size_t ySrc, size_t xDest, size_t yDest)
{
    // 注意这里, 调用at方法时显式指名了父类
    Grid<T>::at(xDest, yDest) = std::move(Grid<T>::at(xSrc, ySrc));
    Grid<T>::at(xSrc, ySrc).reset();  // Reset source cell
    // Or:
    // this->at(xDest, yDest) = std::move(this->at(xSrc, ySrc));
    // this->at(xSrc, ySrc).reset();
}

题外话

  1. 一旦显式定义了析构函数,那么就必须显式定义copy构造函数和copy赋值函数,否则编译器默认将其禁用.

File: _posts/2020-09-27-cpp-casts-note.md


layout: post title: "c++ cast笔记" date: 2020-09-27 21:36:00 +0700 categories: [c++]

const cast

const_cast可以给一个不是const的变量增加const属性, 也可以把一个变量的cast给去掉.
例子:

cpp
extern void ThirdPartyLibraryMethod(char* str);

void f(const char* str)
{
    ThirdPartyLibraryMethod(const_cast<char*>(str));
}

要注意, ThirdPartyLibraryMethod仍然不可以修改参数的值.

下面是std::as_const的例子,它可以把一个非const变量变为const变量.

cpp
std::string str = "C++";
const std::string& constStr = std::as_const(str);

static cast

主要用来进行c++本身就支持的转换, 比如int到double的转换.
也可以用来进行用户自定义的转换, 比如class A的构造器接收一个B对象,那就可以使用static cast
进行B到A的转换.

除此之外, static cast还可以进行继承关系中的向下转型(无关的类型转换不可以使用static_cast).
例子:

cpp
class Base {
public:
    virtual ~Base() = default;    
}

class Derived : public Base {
public:
    virtual ~Derived() = default;
}

int main() {
    Base* b;
    Derived* d = new Derived;
    b = d;
    d = static_cast<Derived*>(b);
    
    Derived derived;
    Base& br = derived;
    Derived& dr = static_cast<Derived&>(br);
}

static_cast作用在继承关系上的转型只可以用于指针和引用.
但是这种转型在某些情况下是不安全的, 比如:

cpp
Base* b = new Base{};
Derived* d = static_cast<Derived*>(b);

这段代码可以编译并执行且不报错,但是却隐藏了极大的未知内存数据被覆盖的风险.

reinterpret cast

可以进行一些static cast不允许的转换.比如将两个毫不相关类型的引用/指针互转.
比如转为void*.
例子:

cpp
class A {};
class B {};

int main() {
    A a;
    B b;
    A* ap = &a;
    B* bp = &b;
    
    // 隐式转换
    void* p = ap;
    // 此时需要使用reinterpret_cast显式转换
    ap = reinterpret_cast<A*>(p);
    
    // 对于毫不相关类型的引用转换也可以使用reinterpret_cast
    A& ar = a;
    B& br = reinterpret_cast<B&>(ar);
    return 0;
}

dynamic cast

提供了运行期的类型检查, 如果不是可以互转的类型那么会:

  1. 返回一个nullptr(指针的情况)
  2. 抛出一个std::bad_cast的异常.(引用的情况)
    例子:
cpp
class Base {
public:
    virtual ~Base() = default;    
}

class Derived : public Base {
public:
    virtual ~Derived() = default;
}

int main() {
    // 正常的情况
    Derived* d = new Derived();
    Base* b = d
    d = dynamic_cast<Derived*>(b);
    
    // 错误的情况
    Base b;
    Base& br = b;
    try {
        Derived& dr = dynamic_cast<Derived&>(br);
    } catch (std::bad_cast&) {
        cout << "bad cast" << endl;
    }
    
    return 0;
}

要注意, 使用dynamic_cast必须保证相关类型有virtual方法,因为类型信息是存储在
对象的vtable中的.


File: _posts/2021-04-14-java-io-model.md

[Binary file]


File: _posts/2021-04-14-java-nio.md


layout: post title: "Java的NIO" date: 2021-04-16 23:07:00 +0700 categories: [java]

NIO概念

NIO指的是在JDK1.4出来之后, 提供的一套可以替代标准IO的New IO API. 由如下三部分组成:

  • Buffer
  • Channel
  • Selector

代码示例

使用NIO大致分为以下几步:

  1. 获取channel并设置为非阻塞模式
  2. 创建selector
  3. 将channel与selector做关联
  4. 调用selector的select方法, 根据返回的状态进行相应的处理
java
// 开启channel,并且设置为非阻塞模式
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
// 绑定端口
serverSocketChannel.bind(new InetSocketAddress(8080));
// 创建selector
Selector selector = Selector.open();
// 将selector和channel关联,这里我们关心从客户端到来的新连接,所以这里设置为ACCEPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (selector.select() > 0) {
    Set<SelectionKey> selectionKeys = selector.selectedKeys();
    Iterator<SelectionKey> iterator = selectionKeys.iterator();
    while (iterator.hasNext()) {
        SelectionKey key = iterator.next();
        if (key.isAcceptable()) {
            // ...
        } else if (key.isReadable()) {
            // ...
        } // 省略...
    }
}

单线程Echo示例

接下来我们编写一个Echo服务的Server端和Client端, 使用单个线程处理新连接和数据.

首先是Server端:

java
Selector selector = initServer();
while (selector.select() > 0) {
    Set<SelectionKey> selectionKeys = selector.selectedKeys();
    Iterator<SelectionKey> iterator = selectionKeys.iterator();
    while (iterator.hasNext()) {
        SelectionKey key = iterator.next();
        if (key.isAcceptable()) {
            ServerSocketChannel channel = (ServerSocketChannel) key.channel();
            SocketChannel clientChannel = channel.accept();
            System.out.println("建立连接:" + clientChannel.getRemoteAddress());
            clientChannel.configureBlocking(false);
            clientChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
        } else if (key.isReadable()) {
            SocketChannel socketChannel = (SocketChannel) key.channel();
            ByteBuffer buffer = (ByteBuffer) key.attachment();
            buffer.clear();
            int read = 0;
            StringBuilder sb = new StringBuilder();
            try {
                while ((read = socketChannel.read(buffer)) > 0) {
                    buffer.flip();
                    sb.append(StandardCharsets.UTF_8.decode(buffer));
                    buffer.clear();
                }
                System.out.printf("收到客户端-(%s) 发来的消息:%s\n", socketChannel.getRemoteAddress(), sb.toString());
                // 返回相同的消息
                buffer.put(("收到,你发来的是:" + sb + "\r\n").getBytes());
                buffer.flip();

                socketChannel.write(buffer);
                socketChannel.register(selector, SelectionKey.OP_READ, buffer.clear());
            } catch (IOException e) {
                e.printStackTrace();
                key.cancel();
                key.channel().close();
            }
        }
        iterator.remove();
    }
}

再来是client端:

java
SocketChannel channel = SocketChannel.open();
channel.connect(new InetSocketAddress("127.0.0.1", 8080));
channel.configureBlocking(false);
Selector selector = Selector.open();
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put(("你好,我是client:" + channel.getLocalAddress() + "\r\n").getBytes());
buffer.flip();
channel.write(buffer);
buffer.clear();
channel.register(selector, OP_READ, buffer);
while (selector.select() > 0) {
    Set<SelectionKey> selectionKeys = selector.selectedKeys();
    Iterator<SelectionKey> iterator = selectionKeys.iterator();
    while (iterator.hasNext()) {
        SelectionKey key = iterator.next();
        if (key.isReadable()) {
            SocketChannel socketChannel = (SocketChannel) key.channel();
            ByteBuffer byteBuffer = (ByteBuffer) key.attachment();
            byteBuffer.clear();
            StringBuilder sb = new StringBuilder();
            while (socketChannel.read(byteBuffer) > 0) {
                byteBuffer.flip();
                sb.append(StandardCharsets.UTF_8.decode(byteBuffer));
                byteBuffer.clear();
            }
            System.out.println("接收到server端的消息:" + sb);

            buffer.put(("你好,我是client:" + channel.getLocalAddress() + "\r\n").getBytes());
            buffer.flip();
            channel.write(buffer);
            buffer.clear();
            channel.register(selector, OP_READ, buffer);
        }
        iterator.remove();
        TimeUnit.MILLISECONDS.sleep(100);
    }
}

Server端输出如下:

text
收到客户端-(/127.0.0.1:61139) 发来的消息:你好,我是client:/127.0.0.1:61139

收到客户端-(/127.0.0.1:61139) 发来的消息:你好,我是client:/127.0.0.1:61139

收到客户端-(/127.0.0.1:61139) 发来的消息:你好,我是client:/127.0.0.1:61139

Client端输出如下:

text
接收到server端的消息:收到,你发来的是:你好,我是client:/127.0.0.1:61139


接收到server端的消息:收到,你发来的是:你好,我是client:/127.0.0.1:61139


接收到server端的消息:收到,你发来的是:你好,我是client:/127.0.0.1:61139

Selector的底层

使用strace命令启动server, 可以看到如下输出:

text
20083 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 4
20084 setsockopt(4, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
20085 clock_gettime(CLOCK_MONOTONIC, {tv_sec=242065, tv_nsec=887240727}) = 0
20086 fcntl(4, F_GETFL)                       = 0x2 (flags O_RDWR)
20087 fcntl(4, F_SETFL, O_RDWR|O_NONBLOCK)    = 0
20088 bind(4, {sa_family=AF_INET, sin_port=htons(9999), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
20089 listen(4, 50)                           = 0
20090 getsockname(4, {sa_family=AF_INET, sin_port=htons(9999), sin_addr=inet_addr("0.0.0.0")}, [16]) = 0
20091 getsockname(4, {sa_family=AF_INET, sin_port=htons(9999), sin_addr=inet_addr("0.0.0.0")}, [16]) = 0
20092 epoll_create(256)                       = 7
21100 epoll_ctl(7, EPOLL_CTL_ADD, 4, {EPOLLIN, {u32=4, u64=158913789956}}) = 0
21101 epoll_wait(7, [{EPOLLIN, {u32=4, u64=158913789956}}], 8192, -1) = 1

可以看到epoll_create、epoll_ctl、epoll_wait的调用.(在linux上的输出,在mac上使用的是kqueue).

多线程处理

我们给当前的server增加一个每20个请求就等待一段时间的逻辑,以此模拟服务端server的卡顿对响应时间造成的影响:

java
if (t.getAndAdd(1) % 20 == 0) {
    TimeUnit.MILLISECONDS.sleep(1000);
}

同时为了模拟并发请求, 我们也对客户端进行改造:

java
int nThread = 100;
for (int i = 0; i < nThread; i++) {
    new Thread(() -> {
        try {
            client(); // 这里做连接server以及发消息
        } catch (Exception e) {
            e.printStackTrace();
        }
    }).start();
}

启动之后观察日志可以发现响应在5s左右:

text
接收到server端的消息:收到,你发来的是:你好,我是client:/127.0.0.1:62221,距离上次:5024
接收到server端的消息:收到,你发来的是:你好,我是client:/127.0.0.1:62256,距离上次:5025
接收到server端的消息:收到,你发来的是:你好,我是client:/127.0.0.1:62199,距离上次:5025

这时我们给server端做一个调整, 读写事件的处理我们交给另外的线程池处理:

java
executorService.execute(() -> {
    SocketChannel socketChannel = (SocketChannel) key.channel();
    if (!key.channel().isOpen()) return;
    try {
        ByteBuffer buffer = (ByteBuffer) key.attachment();
        buffer.clear();
        int read = 0;
        StringBuilder sb = new StringBuilder();
        while ((read = socketChannel.read(buffer)) > 0) {
            buffer.flip();
            sb.append(StandardCharsets.UTF_8.decode(buffer));
            buffer.clear();
        }
        System.out.printf("收到客户端-(%s) 发来的消息:%s\n", socketChannel.getRemoteAddress(), sb.toString());
        // 返回相同的消息
        buffer = ByteBuffer.allocate(1024);
        buffer.put(("收到,你发来的是:" + sb + "\r\n").getBytes());
        buffer.flip();
        TimeUnit.MILLISECONDS.sleep(1000);
        socketChannel.write(buffer);
        socketChannel.register(selector, SelectionKey.OP_READ, buffer.clear());
    } catch (Exception e) {
        e.printStackTrace();
        try {
            key.cancel();
            key.channel().close();
        } catch (Exception ioException) {
            ioException.printStackTrace();
        }
    }
});

示意图如下:

Reactor模式——单线程

The reactor design pattern is an event handling pattern for handling service requests delivered concurrently to a service handler by one or more inputs. The service handler then demultiplexes the incoming requests and dispatches them synchronously to the associated request handlers.

以上是Reactor模式的定义, 可以看到关键点是: 事件分发, 将不同的事件——如读数据、建立连接等分发给不同的handler处理.

如下是Reactor模式的代码示例:

java
static class Reactor {
    private Selector selector;

    private void initServer() {
            try {
                ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
                serverSocketChannel.configureBlocking(false);
                serverSocketChannel.bind(new InetSocketAddress(8080));
                selector = Selector.open();
                serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT, new Acceptor());
            } catch (IOException e) {
                e.printStackTrace();
                throw new RuntimeException(e);
            }
    }

    public void start() throws IOException {
        initServer();
        while (selector.select() > 0) {
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                Handler handler = (Handler) key.attachment();
                if (key.isValid() && handler != null) {
                    handler.process(key);
                }
                iterator.remove();
            }
        }
    }

    interface Handler {
        void process(SelectionKey key);
    }

    class Acceptor implements Handler {

        @Override
        public void process(SelectionKey key) {
            try {
                ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
                SocketChannel clientChannel = serverChannel.accept();
                clientChannel.configureBlocking(false);
                key.attach(new ProcessHandler());
                clientChannel.register(selector, SelectionKey.OP_READ);
                System.out.println("新连接:" + clientChannel.getRemoteAddress());
            } catch (IOException e) {
                key.cancel();
                try {
                    key.channel().close();
                } catch (IOException ioException) {
                    ioException.printStackTrace();
                }
            }
        }
    }

    class ProcessHandler implements Handler {

        @Override
        public void process(SelectionKey key) {
            // 处理逻辑
        }
    }
}

示意图如下:

Reactor模式——多线程

如果我们在接收到数据之后将之后的处理交给线程池进行处理, 那么我们的并发度将进一步提高.
示意图如下:

主从Reacotr模式

上面的多线程Reactor模式其实还有可以改进的, 如果我们将处理新连接的Acceptor和处理数据读写的Handler使用的selector
分开, 这样可以进一步提高网络吞吐量.

示意图如下:


File: _posts/2021-04-14-java-volatile.md

[Binary file]


File: _posts/2021-04-18-java-synchronized.md


layout: post title: "Java关键字synchornized原理解析" date: 2021-04-18 16:30:00 +0700 categories: [java]

字节码

synchornized在编译为class文件时, 对应为两个字节码: monitorenter和monitorexit.

对象头

锁的分类

锁的级别从高到底分别是:

  • 无锁
  • 偏向锁
  • 轻量级锁
  • 重量级锁

对于不同级别的锁状态其MarkWord中的内容也不同, 具体如下.

无锁状态

25bit 4bit 1bit 2bit
对象的hashCode 对象的分代年龄 0 是否是偏向锁 01 锁标志位

偏向锁状态

23bit 2bit 4bit 1bit 2bit
线程id epoch 对象的分代年龄 1 是否是偏向锁 01 锁标志位

轻量级锁

30bit 2bit
指向栈中锁记录的指针 00

重量级锁

30bit 2bit
指向互斥量的指针 10

锁记录与MarkWord

如图所示,该图表达的是如下程序当前的锁记录与锁对象之间的关系.

java
synchronized(obj) {
    synchronized (obj) {
        synchronized (obj) {
            // ...
        }
    }
}

偏向锁

线程在获取锁时会尝试在对象头中设置自己的线程id, 如果设置成功那么之后线程进入退出同步块时就不需要通过CAS来加锁、解锁等,只需要看一下
MarkWord中是否包含自己的线程id即可, 具体流程如下:

  1. 判断当前MarkWord中是否已偏向当前线程, 如果是那么获取锁成功.
  2. CAS将偏向线程改为当前线程(使用的旧值为匿名偏向,即没有偏向任何线程), 如果成功直接返回, 否则存在多线程竞争, 进入轻量级锁模式.

偏向锁的撤销

加锁逻辑
假设线程A当前或者曾经是该偏向锁的持有者, 此时线程B尝试获取锁, 它发现MarkWord中的线程id不是匿名的, 所以首先进入撤销偏向锁的逻辑中,
撤销锁的逻辑如下:

  1. 判断当前线程A是否存活或者在同步块中, 如果未存活, 则先把MarkWord设置为无锁状态, 然后再升级为轻量级锁
  2. 如果存活且在同步块中, 那么将偏向线程的所有Displaced Mark Word设置为null, 同时将处于最高位的LockRecord
    的DisplacedLockRecord设置为无锁状态(最高位的LockRecord就是第一次进入同步块时的LockRecord), 然后将对象头指向
    最高位的LockRecord.

解锁逻辑
从低往高遍历栈的LockRecord,如果当前线程持有的偏向锁没有被撤销(即没有升级为轻量级锁),那么直接将LockRecord释放就好了;
对于已经撤销升级到轻量级锁的,需要将LockRecord中的Displaced Mark Word cas到对象的MarkWord中.
如果这里CAS失败或者是重量级锁, 会在接下来讲解.

轻量级锁

加锁逻辑

  1. 如果是无锁状态会尝试把对象的MarkWord设置到LockRecord的Displaced MarkWord中, 并且把LockRecord的地址设置到对象头的MarkWord中,
    如果这一步成功了, 那么成功获取了锁, 否则膨胀为重量级锁.
  2. 如果是重入状态, 那么此时的LockRecord的Displaced MarkWord设置的是null. 由于是重入状态, 所以不需要cas, 直接返回即可.
  3. 如果在1中失败了或者轻量级锁被其他线程持有, 那么会进入重量级锁逻辑中.

解锁逻辑
从低往高遍历栈的LockRecord进行释放,将最后一个非重入的LockRecord的Displaced MarkWord CAS 到对象头的MarkWord.
CAS失败会进入monitorexit方法.

重量级锁

ObjectMonitor示意图:

膨胀逻辑
从无锁状态或者轻量级锁进入重量级锁要进行锁的膨胀,首先创建一个ObjectMonitor对象, 将它的地址cas到对象头中,
设置LockRecord的Displaced MarkWord为一个特殊值, 代表该锁正在使用一个重量级锁的Monitor.

加锁逻辑

  1. 如果成功CAS设置ObjectMonitor的owner或者是重入, 则直接返回.
  2. 如果CAS失败, 那么进行自旋, 如果通过自旋获取了锁, 那么直接返回.
  3. 自旋失败, 进入ObjectMonitor cxq队列的队首.
  4. 阻塞自己
  5. 被唤醒后再次尝试获取锁.

释放锁逻辑

  1. 将owner字段设置为null
  2. 如果当前没有需要唤醒的线程(entrylist和cxq为空)那么直接返回.
  3. 当前线程重新获取锁,因为之后要操作cxq和entrylist唤醒线程.
  4. 根据策略的不同进行不同的唤醒方法.
    • 策略1: 直接从cxq中获取队首元素唤醒, 结束.
    • 策略2: 把cxq队列插入entrylist尾部
    • 策略3: 把cxq队列插入entrylist头部
    • 策略4: 什么都不做
  5. 如果entrylist不为空, 获取第一个元素唤醒返回.
  6. 否则获取cxq的第一个元素唤醒返回.

总结:synchronized与ReentrantLock的区别

比较点 synchronized ReentrantLock
实现层次 JVM JDK
是否可中断 否 可
公平性 非公平,通常情况下后来线程先获得锁 非公平与公平均可设置
是否可查看锁的状态 否 是
发生异常是否自动释放 是 否,需要手动释放

File: _posts/2021-04-19-java-aqs-shared.md


layout: post title: "Java同步组件AQS原理详解——共享模式" date: 2021-04-19 21:57:00 +0700 categories: [java]

前言

与可重入锁一样,Semaphore的内部也只有一个继承自AQS的Sync类, 也分为公平和非公平模式, 但是和可重入锁不同,
Semaphore对state的操作不同,state在信号量初始化时就设置了一个正整数,表示可以获得的最大资源,每次调用
acquire()或者acquire(int)方法实际上是获取了一定数量的信号量,如果当前信号量不足那么会阻塞等待其他线程释放信号量。

在信号量实现中,同步器也分为公平和非公平模式,我们这里只描述非公平模式的工作流程。

请求信号量

java
public void acquire() throws InterruptedException {
	sync.acquireSharedInterruptibly(1);
}

调用的是AQS的acquireSharedInterruptibly方法,

java
public final void acquireSharedInterruptibly(int arg)
		throws InterruptedException {
	if (Thread.interrupted())
		throw new InterruptedException();
	if (tryAcquireShared(arg) < 0)
		doAcquireSharedInterruptibly(arg);
}

先不关注关于线程中断的处理,可以看到这里先调用了tryAcquireShared方法,该方法的返回值如果是
正数或者0代表请求成功,如果小于0代表请求失败,这里请求失败的话会进入队列等待。
tryAcquireShared和tryAcquire一样需要同步器自定义实现,我们先看tryAcquireShared方法的实现:

java
protected int tryAcquireShared(int acquires) {
	return nonfairTryAcquireShared(acquires);
}

final int nonfairTryAcquireShared(int acquires) {
	for (;;) {
		int available = getState();
		int remaining = available - acquires;
		if (remaining < 0 ||
			compareAndSetState(available, remaining))
			return remaining;
	}
}

实际上调用了nonfairTryAcquireShared方法:

  1. 首先获取当前可用的state
  2. 将当前可用的state减去需要的信号量得到剩余信号量
  3. 如果大于等于0那么尝试直接CAS否则返回剩余的值,如果成功直接返回否则重试
  4. 重试直到剩余数量小于0或者CAS成功。

假设这里剩余数量小于0,回到代码:

java
if (tryAcquireShared(arg) < 0)
	doAcquireSharedInterruptibly(arg);

进入doAcquireSharedInterruptibly方法,该方法是AQS内部实现:

java
private void doAcquireSharedInterruptibly(int arg)
	throws InterruptedException {
	final Node node = addWaiter(Node.SHARED); // @1
	boolean failed = true;
	try {
		for (;;) {
			final Node p = node.predecessor(); // @2
			if (p == head) {
				int r = tryAcquireShared(arg);
				if (r >= 0) {
					setHeadAndPropagate(node, r);
					p.next = null; // help GC
					failed = false;
					return;
				}
			}
			if (shouldParkAfterFailedAcquire(p, node) &&
				parkAndCheckInterrupt())
				throw new InterruptedException();
		}
	} finally {
		if (failed)
			cancelAcquire(node);
	}
}
  1. 创建了一个共享模式的节点,加入AQS的内部链表中
  2. 进入循环尝试,如果当前节点的前面是Head节点,说明此时有可能竞争成功,此时调用tryAcquireShared方法,该方法上面已经描述,此处不再赘述。如果
    成功那么调用setHeadAndPropagate方法,将当前节点设置为Head,并且可能唤醒后续节点(这里我们之后再详述)。
  3. 如果前驱节点不是Head或者是Head但是尝试获取失败,那么会进入shouldParkAfterFailedAcquire方法,将前驱节点的状态更新为SIGNAL,然后再尝试一遍当前的逻辑,
    如果还是获取不到那么就阻塞自己,等待其他线程释放唤醒自己。

要注意共享模式和独占模式的区别在于,共享模式允许多个Node同时持有state的一部分,
在一个线程获取成功时可能其他线程在释放信号量,那么要注意此时入队的线程,如果可以让它获得信号量的话一定要唤醒它。

具体进入setHeadAndPropagate方法:

java
private void setHeadAndPropagate(Node node, int propagate) {
	Node h = head; 
	// 更新head,相当于出队
	setHead(node);
	// 当1.剩余信号量大于0
	// 2.当前head是PROPAGATE或SIGNAL
	// 3.当前节点是SIGNAL
	// 唤醒后继节点(如果有的话)
	if (propagate > 0 || h == null || h.waitStatus < 0 ||
		(h = head) == null || h.waitStatus < 0) {
		Node s = node.next;
		if (s == null || s.isShared())
			doReleaseShared();
	}
}

在这里做了一系列判断是否要唤醒后继,如果这里判断不对,而恰好在判断时,后继节点即将阻塞而释放线程
在其即将阻塞的时候释放,那么这时后继节点永远也得不到唤醒。
在这里我们有几个比较重要的判断propagate > 0、oldHead.waitStatus < 0, newHead.waitStatus < 0
对于第一个条件,比较好理解,因为这意味着还有剩余的信号量,所以去唤醒。
对于第二个条件,oldHead的status为负数代表其为SIGNAL或者PROPAGATE,如果是SIGNAL代表有后继节点等待唤醒,这时去唤醒,
如果为PROPAGATE,那么代表此时有另外一个线程在释放,将Head的状态由0更新为PROPAGATE
这里的意思是虽然没有后继节点但是可能后继节点还没有更新前驱的状态为SIGNAL,设置这个值就是尽快让后继节点苏醒。
第三个条件比较好理解,因为此时更新当前节点的状态的只有后继节点或者新的释放线程,要么更新为SIGNAL代表需要唤醒后继,
要么为PROPAGATE,代表有线程释放了信号量,这两种情况都要唤醒后继。

具体如何唤醒呢,下面进入了doReleaseShared方法,该方法在线程释放信号量时也会调用。

java
private void doReleaseShared() {
	for (;;) {
		// 获取当前head
		Node h = head;
		// 队列中存在等待节点的话进入下面的逻辑
		if (h != null && h != tail) {
			int ws = h.waitStatus;
			// 如果是SIGNAL代表有节点等待被唤醒
			if (ws == Node.SIGNAL) {
				// 尝试CAS,如果失败,那么可能其他线程也执行到了这里,重试
				if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
					continue;
				// 唤醒后继节点	
				unparkSuccessor(h);
			}
			// 如果ws==0,意味着后面可能存在节点在尝试获取但是可能还没有
			// 更新前驱为SIGNAL,此时为了快速唤醒线程,更新为PROPAGATE
			else if (ws == 0 &&
					 !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
				continue;
		}
		if (h == head) 
			break;
	}
}

网上有人说使用PROPAGATE是为了防止在两个线程请求两个线程释放的情况下队列hang住的问题,实际上即使
不使用PROPAGATE在setHeadAndPropagate方法中的5个判断也完全不会发生hang住的情况。

总结

以上就是Semaphore以及AQS共享模式的内部原理, 接下来我会接着对读写锁、CountDownLatch、CyclicBarrier进行分析.