排序
十大基本排序算法:冒泡、选择、插入、希尔、归并、快速、堆、计数、桶、基数
稳定:冒泡、插入、归并和基数。 不稳定:选择、快速、希尔、堆。
冒泡排序
正序时最快,反序时最慢。
def bubbleSort(arr):
for i in range(1, len(arr)):
for j in range(0, len(arr)-i):
if arr[j] > arr[j+1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
return arr
golang实现:
func bubbleSort(arr []int) []int {
length := len(arr)
for i := 0; i < length; i++ {
for j := 0; j < length-1-i; j++ {
if arr[j] > arr[j+1] {
arr[j], arr[j+1] = arr[j+1], arr[j]
}
}
}
return arr
}
选择排序
选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
def selectionSort(arr):
for i in range(len(arr) - 1):
---
title: 记录最小数的索引
minIndex = i
for j in range(i + 1, len(arr)):
if arr[j] < arr[minIndex]:
minIndex = j
---
title: i 不是最小数时,将 i 和最小数进行交换
if i != minIndex:
arr[i], arr[minIndex] = arr[minIndex], arr[i]
return arr
上述是使用递归的解法,应该还有更好的方法。非递归版本:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummyHead = new ListNode(-1);
ListNode cur = dummyHead;
while(l1 != null && l2 != null){
if(l1.val<l2.val){
cur.next = l1;
l1 = l1.next;
cur = cur.next;
}
else{
cur.next = l2;
l2 = l2.next;
cur = cur.next;
}
}
if(l1 == null){
cur.next = l2;
}
else{
cur.next = l1;
}
return dummyHead.next;
}
}
非递归解法好像跟容易理解一些。python实现:
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
new_head = ListNode(-1, None)
cur = new_head
while l1 != None and l2 != None:
if l1.val < l2.val:
cur.next = l1
l1 = l1.next
cur = cur.next
else:
cur.next = l2
l2 = l2.next
cur = cur.next
# 处理剩余的
if l1 == None:
cur.next = l2
else:
cur.next = l1
return new_head.next
环形链表
判断链表中是否有环。典型的快慢指针题目。
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null)
return false;
ListNode slow = head;
ListNode fast = head.next;
while(fast != slow){
if (fast == null || fast.next == null){
return false;
}
slow = slow.next;
fast = fast.next.next;
}
return true;
}
}
python实现:
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def hasCycle(self, head: ListNode) -> bool:
if head is None:
return False
fast = head.next
while fast != None and head != None and fast.next != None:
if fast == head:
return True
fast = fast.next.next
head = head.next
return False
环形链表②
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
说明:不允许修改给定的链表。
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def detectCycle(self, head: ListNode) -> ListNode:
fast = head
slow = head
while True:
# 两个都为空时直接返回,说明没有环
if not (fast and fast.next):
return
fast = fast.next.next
slow = slow.next
if fast == slow:
# 说明相遇了
break
fast = head
# 第二次相遇时fast指向的节点就是环的入口节点
while fast != slow:
fast = fast.next
slow = slow.next
return fast
动态规划
爬楼梯
状态转移方程:dp[n] =dp[n-1] + dp[n-2]
上1阶台阶:有1种方式
上2阶台阶:有1+1和2两种方式
上n阶台阶:到达第n阶的方法总数就是到第n-1阶和第n-2阶的方法数之和。
func climbStairs(n int) int {
if n ==1 {
return 1
}
dp := make([]int, n+1)
dp[1] = 1
dp[2] = 2
for i:=3; i<=n; i++ {
dp[i] = dp[i-1] + dp[i-2]
}
return dp[n]
}
位运算
常用的位运算技巧:
1、使用 x & 1 == 1 判断奇偶数。(注意,一些编辑器底层会把用%判断奇偶数的代码,自动优化成位运算)
2、不使用第三个数,交换两个数。x = x ^ y , y = x ^ y , x = x ^ y。(早些年喜欢问到,现在如果谁再问,大家会觉得很low)
3、两个相同的数异或的结果是 0,一个数和 0 异或的结果是它本身。(对于找数这块,异或往往有一些别样的用处。)
异或的运算符是^
。
4、x & (x - 1) ,可以将最右边的 1 设置为 0。(这个技巧可以用来检测 2的幂,或者检测一个整数二进制中 1 的个数,又或者别人问你一个数变成另一个数其中改变了多少个bit位,统统都是它)
对于N为2的幂的数,都有N&(N-1)==0
。
5、异或可以被当做无进位加法使用,与操作可以用来获取进位。
6、i+(~i)=-1,i 取反再与 i 相加,相当于把所有二进制位设为1,其十进制结果为-1。
7、对于int32而言,使用 n >> 31取得 n 的正负号。并且可以通过 (n ^ (n >> 31)) - (n >> 31) 来得到绝对值。(n为正,n >> 31 的所有位等于0。若n为负数,n >> 31 的所有位等于1,其值等于-1)
8、使用 (x ^ y) >= 0 来判断符号是否相同。(如果两个数都是正数,则二进制的第一位均为0,xy=0;如果两个数都是负数,则二进制的第一位均为1;xy=0 如果两个数符号相反,则二进制的第一位相反,xy=1。有0的情况例外,相同得0,不同得1)
判断一个数是否是2的幂
实现:
class Solution:
def isPowerOfTwo(self, n: int) -> bool:
return n >0 and n & (n-1) == 0
位1的个数
class Solution:
def singleNumber(self, nums: int) -> int:
result = 0
mask = 1 # 设置一个掩码就可以
for i in range(32):
if n&mask != 0:
result+=1
mask = mask << 1
return result
只出现一次的数字
class Solution:
def singleNumber(self, nums: List[int]) -> int:
# 1. 自己跟自己异或结果是0
# 2. 任何一个数跟0异或结果是自己本身
ans = 0
for i in nums:
ans = ans ^ i
return ans
栈相关
基本计算器
实现一个基本的计算器来计算一个简单的字符串表达式的值。
字符串表达式可以包括以下字符:
左括号
右括号
加号
减号
非负整数
空格
解题思路:
使用两个栈,一个用来存数字和左括号nums
,一个用来存操作符ops
。当nums
的最后两个元素是数字并且ops
非空,就把这三个元素弹出来求值一次,再把结果压入nums
。如果碰到右括号,则删除nums
的倒数第二个元素(这个元素一定是左括号),再尝试求值一次。
实现:
class Solution:
def evaluate_expr(self, stack):
res = stack.pop() if stack else 0
while stack and stack[-1] != ')':
sign = stack.pop()
if sign == '+':
res += stack.pop()
else:
res -= stack.pop()
return res
def calculate(self, s: str) -> int:
stack = []
# n为数字的位数, operand为一个计算数的值
n, operand = 0, 0 # 初始化值
for i in range(len(s)-1, -1, -1):
# [len(s)-1, ... 0]
ch = s[i]
# 1.数字时
if ch.isdigit():
operand = (10**n * int(ch)) + operand
n += 1
# 2.操作符时
elif ch != " ":
# 说明是多个数字
if n:
stack.append(operand)
n, operand = 0, 0 # 恢复初始值
if ch == '(':
res = self.evaluate_expr(stack)
stack.pop()
stack.append(res)
else:
# 说明是单个数字
stack.append(ch)
# 最后一个数字也要考虑
if n:
stack.append(operand)
return self.evaluate_expr(stack)
进一步思考,要求打印输出最长连续递增序列应该怎么办?
合并两个有序链表
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if(l1 == null) return l2;
if(l2 == null) return l1;
if(l1.val < l2.val){
l1.next = mergeTwoLists(l1.next, l2);
return l1;
}else{
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
}
上述是使用递归的解法,应该还有更好的方法。非递归版本:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummyHead = new ListNode(-1);
ListNode cur = dummyHead;
while(l1 != null && l2 != null){
if(l1.val<l2.val){
cur.next = l1;
l1 = l1.next;
cur = cur.next;
}
else{
cur.next = l2;
l2 = l2.next;
cur = cur.next;
}
}
if(l1 == null){
cur.next = l2;
}
else{
cur.next = l1;
}
return dummyHead.next;
}
}
非递归解法好像跟容易理解一些。python实现:
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
new_head = ListNode(-1, None)
cur = new_head
while l1 != None and l2 != None:
if l1.val < l2.val:
cur.next = l1
l1 = l1.next
cur = cur.next
else:
cur.next = l2
l2 = l2.next
cur = cur.next
# 处理剩余的
if l1 == None:
cur.next = l2
else:
cur.next = l1
return new_head.next
环形链表
判断链表中是否有环。典型的快慢指针题目。
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null)
return false;
ListNode slow = head;
ListNode fast = head.next;
while(fast != slow){
if (fast == null || fast.next == null){
return false;
}
slow = slow.next;
fast = fast.next.next;
}
return true;
}
}
python实现:
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def hasCycle(self, head: ListNode) -> bool:
if head is None:
return False
fast = head.next
while fast != None and head != None and fast.next != None:
if fast == head:
return True
fast = fast.next.next
head = head.next
return False
环形链表②
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
说明:不允许修改给定的链表。
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def detectCycle(self, head: ListNode) -> ListNode:
fast = head
slow = head
while True:
# 两个都为空时直接返回,说明没有环
if not (fast and fast.next):
return
fast = fast.next.next
slow = slow.next
if fast == slow:
# 说明相遇了
break
fast = head
# 第二次相遇时fast指向的节点就是环的入口节点
while fast != slow:
fast = fast.next
slow = slow.next
return fast
什么是ZooKeeper
ZooKeeper是一个典型的发布/订阅模式的分布式数据管理与协调框架。
ZooKeeper是Apache社区顶级的开源项目,主要用于分布式系统,可以用ZooKeeper来做:统一配置管理、统一命名服务、分布式锁、集群管理。
ZooKeeper解决了什么问题?
- 高性能使得ZK能够应用于对系统吞吐有明确要求的大型分布式系统,如云计算
- 可以解决分布式的单点问题
- 具有严格的顺序访问控制能力,主要是针对写操作的严格顺序性,使得客户端可以基于ZooKeeper来实现一些复杂的同步原语。
- 具备原子性,更新操作要么成功,要么失败,没有中间状态。
ZooKeeper的基本概念
跟Unix文件系统非常相似,可以看做是一棵树,每个节点叫做ZNode。每个节点通过路径来标识。
ZNode节点分为两种类型:
- 短暂/临时(Ephemeral)
- 持久(Persistent)
区别:当客户端与服务端断开连接后,前者所创建的ZNode会自动删除;后者会保留。
Session会话:客户端启动会与服务器端建立一个TCP长连接,通过这个连接可以发送请求并接受响应,以及接受服务端的Watcher事件通知
Watcher事件监听器:客户端可以在ZNode上注册Watcher,服务端将事件通知已注册的客户端
优点
可靠,可扩展,高性能。
遵循C/S模型(C指client,使用zk服务的机器,S指server,提供zk服务的机器)。
ZK可以提供单机服务,也可组成集群提供服务,还支持伪集群方式(一台物理机运行多个zookeeper实例)。客户端连接到一个单独的服务。客户端保持了一个TCP连接,通过这个TCP连接发送请求、获取响应、获取watch事件、和发送心跳。如果这个连接断了,会自动连接到其他不同的服务器。
监听器
监听器监听节点ZNode的变化,主要有以下两项:
- 监听ZNode节点的数据变化
- 监听子节点的增减变化
前者是监听某个节点数据本身是否发生了变化,后者监听某个节点的子节点是否发生了增减。ZooKeeper通过这两个监听就能完成很多工作了。
角色
-
leader -- 领导者
-
learner -- 学习者
- 跟随者 -- follower
- 观察者 -- observer
-
client -- 客户端
leader选举采用paxos协议,核心思想是:当多数server写成功,那么数据写入成功。
如果有3个服务器,那么有两个写入成功,则数据写入成功。如果有4个或5个服务器,有3个写成功,则数据写成功。
server数量一般为奇数,如3、5、7等。
ZAB协议
ZooKeeper Atomic Broadcast,保证了ZooKeeper集群数据的一致性和命令的全局有序性。
-
ZAB状态
Zookeeper还给ZAB定义的4中状态,反应Zookeeper从选举到对外提供服务的过程中的四个步骤。状态枚举定义:
public enum ZabState {
ELECTION,
DISCOVERY,
SYNCHRONIZATION,
BROADCAST
}
- ELECTION: 集群进入选举状态,此过程会选出一个节点作为leader角色;
- DISCOVERY:连接上leader,响应leader心跳,并且检测leader的角色是否更改,通过此步骤之后选举出的leader才能执行真正职务;
- SYNCHRONIZATION:整个集群都确认leader之后,将会把leader的数据同步到各个节点,保证整个集群的数据一致性;
- BROADCAST:过渡到广播状态,集群开始对外提供服务。
- ZXID
Zxid是极为重要的概念,它是一个long型(64位)整数,分为两部分:纪元(epoch)部分和计数器(counter)部分,是一个全局有序的数字。
epoch代表当前集群所属的哪个leader,leader的选举就类似一个朝代的更替,你前朝的剑不能斩本朝的官,用epoch代表当前命令的有效性,counter是一个递增的数字。
-
什么时候进行选举?选举发生的时机有两个:
一个是服务启动的时候当整个集群都没有leader节点会进入选举状态,如果leader已经存在就会告诉该节点leader的信息,自己连接上leader,整个集群不用进入选举状态。
还有一个就是在服务运行中,可能会出现各种情况,服务宕机、断电、网络延迟很高的时候leader都不能再对外提供服务了,所有当其他几点通过心跳检测到leader失联之后,集群也会进入选举状态。
-
选举规则
下面代码是zookeeper投票比较规则:
/* * We return true if one of the following three cases hold: * 1- New epoch is higher * 2- New epoch is the same as current epoch, but new zxid is higher * 3- New epoch is the same as current epoch, new zxid is the same * as current zxid, but server id is higher. */ return ((newEpoch > curEpoch) || ((newEpoch == curEpoch) && ((newZxid > curZxid) || ((newZxid == curZxid) && (newId > curId)))));
当其他节点的纪元比自身高投它,如果纪元相同比较自身的zxid的大小,选举zxid大的节点,这里的zxid代表节点所提交事务最大的id,zxid越大代表该节点的数据越完整。
最后如果epoch和zxid都相等,则比较服务的serverId,这个Id是配置zookeeper集群所配置的,所以我们配置zookeeper集群的时候可以把服务性能更高的集群的serverId配置大些,让性能好的机器担任leader角色。
-
选举流程
所有节点第一票先选举自己当leader,将投票信息广播出去;
从队列中接受投票信息;
按照规则判断是否需要更改投票信息,将更改后的投票信息再次广播出去;
判断是否有超过一半的投票选举同一个节点,如果是选举结束根据投票结果设置自己的服务状态,选举结束,否则继续进入投票流程。
-
状态流转
前面介绍了zookeeper服务状态有四种,ZAB状态也有四种。这里就简单介绍一个他们之间的状态流转,更能加深对zab协议在zookeeper工作流程中的作用。
- 服务在启动或者和leader失联之后服务状态转为LOOKING;
- 如果leader不存在选举leader,如果存在直接连接leader,此时zab协议状态为ELECTION;
- 如果有超过半数的投票选择同一台server,则leader选举结束,被选举为leader的server服务状态为LEADING,其他server服务状态为FOLLOWING/OBSERVING;
- 所有server连接上leader,此时zab协议状态为DISCOVERY;
- leader同步数据给learner,使各个从节点数据和leader保持一致,此时zab协议状态为SYNCHRONIZATION;
- 同步超过一半的server之后,集群对外提供服务,此时zab状态为BROADCAST。
可以知道整个zookeeper服务的工作流程类似一个状态机的转换,而zab协议就是驱动服务状态流转的关键,理解了zab就理解了zookeeper工作的关键原理。
ZooKeeper典型应用
利用ZooKeeper可以非常方便构建一系列分布式应用中都会涉及到的核心功能。
- 数据发布/订阅
- 负载均衡
- 命名服务
- 分布式协调/通知
- 集群管理
- Master选举
- 分布式锁
- 分布式队列
多个开源项目中都应用到了ZooKeeper,例如HBase、Spark、Flink、Storm、Kafka、Dubbo等等。
数据发布/订阅
一个常见的场景是配置中心,发布者把数据发布到ZooKeeper的一个或一系列的节点上,供订阅者进行数据订阅,达到动态获取数据的目的。
配置信息一般有几个特点:
-
数据量小的KV
-
数据内容在运行时会发生动态变化
-
集群机器共享,配置一致
ZooKeeper 采用的是推拉结合的方式。
- 推: 服务端会推给注册了监控节点的客户端 Wathcer 事件通知
- 拉: 客户端获得通知后,然后主动到服务端拉取最新的数据
具体实现步骤:
- 把配置信息写到一个ZNode上,例如/Configuration
- 客户端启动初始化阶段读取服务端节点的数据,并且注册一个数据变更的Watcher
- 配置发生变更时,只需要对ZNode数据进行set操作,数据变更的通知会发送到客户端,客户端重新获取新数据,完成配置的动态修改
负载均衡
负载均衡是一种手段,用来把对某种资源的访问分摊给不同的设备,从而减轻单点的压力。
实现的思路:
- 首先建立 Servers 节点,并建立监听器监视 Servers 子节点的状态(用于在服务器增添时及时同步当前集群中服务器列表)
- 在每个服务器启动时,在 Servers 节点下建立临时子节点 Worker Server,并在对应的字节点下存入服务器的相关信息,包括服务的地址,IP,端口等等
- 可以自定义一个负载均衡算法,在每个请求过来时从 ZooKeeper 服务器中获取当前集群服务器列表,根据算法选出其中一个服务器来处理请求
命名服务
命名服务就是提供名称的服务。ZooKeeper的命名服务有两个应用方面。
-
提供类JNDI功能,可以把系统中各种服务的名称、地址以及目录信息存放在ZooKeeper,需要的时候去ZooKeeper中读取。类似于一个公共存储空间。
-
利用 ZooKeeper 顺序节点的特性,制作分布式的序列号生成器,或者叫 id 生成器。(分布式环境下使用作为数据库 id,另外一种是 UUID(缺点:没有规律)),ZooKeeper 可以生成有顺序的容易理解的同时支持分布式环境的编号。
在创建节点时,如果设置节点是有序的,则 ZooKeeper 会自动在你的节点名后面加上序号,上面说容易理解,是比如说这样,你要获得订单的 id,你可以在创建节点时指定节点名为 order_[日期]_xxxxxx,这样一看就大概知道是什么时候的订单。
统一配置管理
典型场景:各个独立的小系统监听使用同一份配置文件。
比如我们现在有三个系统A、B、C,他们有三份配置,分别是ASystem.yml、BSystem.yml、CSystem.yml
,然后,这三份配置又非常类似,很多的配置项几乎都一样。
此时,如果我们要改变其中一份配置项的信息,很可能其他两份都要改。很容易遗漏。
于是,我们希望把ASystem.yml、BSystem.yml、CSystem.yml
相同的配置项抽取出来成一份公用的配置common.yml
,并且即便common.yml
改了,系统A、B、C能够自动获取到更改,分别进行自我更新。
做法:可以将common.yml
这份配置放置到ZooKeeper的某个ZNode节点中,系统A、B、C监听该ZNode节点有无变更,如果变更了,及时自我响应处理。
这个应用应该很容易理解。
分布式协调/通知
一种典型的分布式系统机器间的通信方式是心跳。
心跳检测是指,在分布式环境中,不同机器之间需要检测彼此是否正常运行。
传统的方法是通过主机之间相互PING来实现,又或者是建立长链接,通过TCP连接固有的心跳检测机制来实现上层机器的心跳检测。
如果使用ZooKeeper,可以基于其临时节点的特性,不同机器在ZooKeeper的一个指定节点下创建临时子节点,不同机器之间可以根据这个临时节点来判断客户端机器是否存活。
好处就是检测系统和被检系统不需要直接相关联,而是通过ZooKeeper节点来关联,大大减少系统的耦合。
分布式锁
可以使用ZooKeeper来实现分布式锁。
系统A、B、C都去访问/locks
节点
访问的时候会创建带顺序号的临时/短暂(EPHEMERAL_SEQUENTIAL
)节点,比如,系统A创建了id_000000
节点,系统B创建了id_000002
节点,系统C创建了id_000001
节点。
创建临时节点时,节点的顺序号是逐渐递增的,多个访问总会有时间先后顺序,先创建的临时节点序号低。
接着,每个系统访问/locks拿到所有子节点,并判断自己创建的是不是最小的子节点。
-
如果是,则拿到锁。
释放锁:执行相关业务完成后,把创建的节点给删除掉。
-
如果不是,则监听比自己小1的节点的变化,直到自己是最小的临时节点。
典型应用是秒杀。zk节点有个唯一的特性,就是创建过这个节点了,你再创建zk是会报错的。
10个线程全部去创建,创建成功的第一个返回true,他就可以继续下面的扣减库存操作,后续的节点访问就会全部报错,扣减失败,我们把它们丢一个队列去排队。
怎么释放锁呢?
删除节点,删除了再通知其他人过来加锁,依次类推。
集群状态管理
集群管理主要指集群监控和集群控制两个方面。前者侧重于集群运行时的状态的收集,后者则是对集群进行操作与控制。开发和运维中,面对集群,经常有如下需求:
- 希望知道集群中究竟有多少机器在工作
- 对集群中的每台机器的运行时状态进行数据收集
- 对集群中机器进行上下线的操作
分布式集群管理体系中有一种传统的基于 Agent 的方式,就是在集群每台机器部署 Agent 来收集机器的 CPU、内存等指标。但是如果需要深入到业务状态进行监控,比如一个分布式消息中间件中,希望监控每个消费者对消息的消费状态,或者一个分布式任务调度系统中,需要对每个机器删的任务执行情况进行监控。对于这些业务紧密耦合的监控需求,统一的 Agent 是不太合适的。
利用 ZooKeeper 实现集群管理监控组件的思路:
在管理机器上线/下线的场景中,为了实现自动化的线上运维,我们必须对机器的上/下线情况有一个全局的监控。通常在新增机器的时候,需要首先将指定的 Agent 部署到这些机器上去。Agent 部署启动之后,会首先向 ZooKeeper 的指定节点进行注册,具体的做法就是在机器列表节点下面创建一个临时子节点,例如 /machine/[Hostname]
(下文我们以“主机节点”代表这个节点),如下图所示。当 Agent 在 ZooKeeper 上创建完这个临时子节点后,对 /machines
节点关注的监控中心就会接收到“子节点变更”事件,即上线通知,于是就可以对这个新加入的机器开启相应的后台管理逻辑。另一方面,监控中心同样可以获取到机器下线的通知,这样便实现了对机器上/下线的检测,同时能够很容易的获取到在线的机器列表,对于大规模的扩容和容量评估都有很大的帮助。
一般用在集群操作系统中,比如云计算等领域,实现故障节点自动检测、自动下线,新增节点自动添加、自动注册管理。
如一个云操作系统OpenStack,节点共有100+,实时监控每个节点VM是否正常在线,如果不在需要及时上报告警。
比如节点A会有client守护进程,写心跳到ZooKeeper的ZNode X来维持它临时节点的存在;如果节点A宕机断电等异常场景下,肯定不会写心跳上报Zookeeper状态,临时节点超期就会自动删除。那么,其他监控这个ZNode X父节点的就会发现异常,并作出响应。
可以感知节点的上下线变化。把每个节点VM换成每个独立服务也是一样的。
主备Master动态选举
利用 ZooKeeper 创建节点 API 接口,提供了强一致性,能够很好保证在分布式高并发情况下节点的创建一定是全局唯一性。集群机器都尝试创建节点,创建成功的客户端机器就会成为 Master,失败的客户端机器就在该节点上注册一个 Watcher 用于监控当前 Master 机器是否存活,一旦发现 Master 挂了,其余客户端就可以进行选举了。
Zookeeper会每次选举最小编号的作为Master,如果Master挂了,自然对应的Znode节点就会删除。然后让新的最小编号作为Master,这样就可以实现动态选举的功能了。
原理也很简单,如果想要实现动态选举Master的功能,Znode节点的类型是带顺序号的临时节点(EPHEMERAL_SEQUENTIAL
)就好了。
一般在某个核心服务主备部署场景下很有用,主服务和备服务一般部署在不同机器上,然后分别向ZooKeeper某个ZNode创建子临时节点(带顺序号),并监控该ZNode的变化。临时节点小的为主服务,大的为备服务。
一般是一主一备,或者一主多备部署。
当异常发生时,主服务挂掉,主服务的临时节点也会自动删除,其他服务会选择新的最小编号作为Master,某个备升级成Master。当原来的主服务修复后,会重新创建子临时节点,这时顺序号比较大,作为新的备服务。这样就大大提高了服务的可靠性。
在大型分布式系统中的应用
Hadoop
Hadoop 利用 ZooKeeper 实现了高可用的功能,包括 HDFS 的 NameNode 和 YARN 的 ResourceManager。此外 YARN 的运行状态是利用 ZooKeeper 来存储的。
Kafka
Kafka 中大部分组件都应用了 ZooKeeper。
- Broker 注册 `/broker/ids/[0...N] 记录了 Broker 服务器列表记录,这个临时节点的节点数据是 ip 端口之类的信息。
- Topic 注册
/broker/topcs
记录了 Topic 的分区信息和 Broker 的对应关系 - 生产者负载均衡 生产者需要将消息发送到对应的 Broker 上,生产者通过 Broker 和 Topic 注册的信息,以及 Broker 和 Topic 的对应关系和变化注册事件 Watcher 监听,从而实现一种动态的负载均衡机制。
- 消息消费进度 Offset 记录 消费者对指定消息分区进行消息消费的过程中,需要定时将分区消息的消费进度 Offset 记录到 ZooKeeper 上,以便消费者进行重启或者其他消费者重新阶段该消息分区的消息消费后,能够从之前的进度开始继续系消费
Dubbo
Dubbo 基于 ZooKeeper 实现了服务注册中心。哪一个服务由哪一个机器来提供必需让调用者知道,简单来说就是 ip 地址和服务名称的对应关系。ZooKeeper 通过心跳机制可以检测挂掉的机器并将挂掉机器的 ip 和服务对应关系从列表中删除。
至于支持高并发,简单来说就是横向扩展,在不更改代码的情况通过添加机器来提高运算能力。通过添加新的机器向 ZooKeeper 注册服务,服务的提供者多了能服务的客户就多了。
总结
由于ZooKeeper可以很好保证分布式环境中数据的强一致性,也是基于这个特点,使得越来越多的分布式系统将ZooKeeper作为核心组件进行封装使用。在以上提到的这些分布式系统的常见的应用场景下,利用ZooKeeper可以快速的实现相关的组件,而无需重新造轮子。
很多大型开源的分布式系统中应用ZooKeeper来实现组件的时候,一般都会考虑使用Apache Curator,是Netfix公司开源的一个ZooKeeper客户端,提供了更高层次和更易用的操作ZooKeeper接口。
通过ZNode的节点的类型+监听机制就可以实现很多好用的功能,不得不说是一个很天才的设计。
参考文档
-
什么是ZooKeeper?
-
分布式服务框架 Zookeeper
https://www.ibm.com/developerworks/cn/opensource/os-cn-zookeeper/index.html
-
ZooKeeper初识整理(老酒装新瓶)
-
ZooKeeper
-
ZooKeeper 的应用场景