利用正则表达式解析zookeeper四字命令输出

zookeeper支持四字命令获取节点的内部状态,所谓的四字命令,就是往zookeeper配置的ip和服务端口发送srvr/ruok之类的字符串,zookeeper返回内部状态的命令。具体的有:

conf:输出相关服务配置的详细信息。比如端口、zk数据及日志配置路径、最大连接数,session超时时间、serverId等。

cons:列出所有连接到这台服务器的客户端连接/会话的详细信息。包括“接受/发送”的包数量、session id 、操作延迟、最后的操作执行等信息

crst:重置当前这台服务器所有连接/会话的统计信息

dump:列出未经处理的会话和临时节点(只在leader上有效)

envi:输出关于服务器的环境详细信息(不同于conf命令),比如host.name、java.version、java.home、user.dir=/data/zookeeper-3.4.6/bin之类信息

ruok:测试服务是否处于正确运行状态。如果正常返回”imok”,否则返回空

srst:重置服务器的统计信息

srvr:输出服务器的详细信息。zk版本、接收/发送包数量、连接数、模式(leader/follower)、节点总数

stat:输出服务器的详细信息:接收/发送包数量、连接数、模式(leader/follower)、节点总数、延迟。 所有客户端的列表

wchs:列出服务器watches的简洁信息:连接总数、watching节点总数和watches总数

wchc:通过session分组,列出watch的所有节点,它的输出是一个与 watch 相关的会话的节点列表。如果watches数量很大的话,将会产生很大的开销,会影响性能,小心使用

wchp:通过路径分组,列出所有的 watch 的session id信息。它输出一个与 session 相关的路径。如果watches数量很大的话,将会产生很大的开销,会影响性能,小心使用

mntr:列出集群的健康状态。包括“接受/发送”的包数量、操作延迟、当前服务模式(leader/follower)、节点总数、watch总数、临时节点总数

现在如果需要在go代码中,通过四字命令获取服务器状态,请求应答的过程比较简单,提取具体数字的过程考验对正则表达式的理解。

[root@VM_15_146_centos ~]# echo srvr | nc 127.0.0.1 2283
Zookeeper version: 3.4.14-4c25d480e66aadd371de8bd2fd8da255ac140bcf, built on 03/06/2019 16:18 GMT
Latency min/avg/max: 0/0/56
Received: 7274380
Sent: 7274376
Connections: 4
Outstanding: 0
Zxid: 0x1000002a19
Mode: follower
Node count: 23228

上面是执行一次srvr命令得到的输出,我们要从这个字符串(tcp连接读出的[]byte)中解析出版本、构建时间和其他所有指标。

const (
		zrVer   = `^Zookeeper version: ([A-Za-z0-9\.\-]+), built on (\d\d/\d\d/\d\d\d\d \d\d:\d\d [A-Za-z0-9:\+\-]+)`
		zrLat   = `^Latency min/avg/max: (\d+)/(\d+)/(\d+)`
		zrNet   = `^Received: (\d+).*\n^Sent: (\d+).*\n^Connections: (\d+).*\n^Outstanding: (\d+)`
		zrState = `^Zxid: (0x[A-Za-z0-9]+).*\n^Mode: (\w+).*\n^Node count: (\d+)`
	)

re, err := regexp.Compile(fmt.Sprintf(`(?m:\A%v.*\n%v.*\n%v.*\n%v)`, zrVer, zrLat, zrNet, zrState))

matches := re.FindAllStringSubmatch(string(response), -1)

在这些正则表达式中,()扩起的部分,在FindAllStringSubmatch调用中,将会按照二维数组方式返回,自己数是第几个,取到对应的字段。

这个字符串确定只会匹配上一次,因此可以直接使用matches[0]赋值将匹配结果降为一维数组,数组元素就是各个字段的值。

在上面,match[0]是版本号,match[1]是构建时间,以此类推下去。

Fisher–Yates shuffle 洗牌算法 和go-zookeeper库中的代码缺陷

项目中使用到一个golang的zookeeper库,使用过程中遇到一些问题待解决,看看源代码解决。记录下看源代码过程看到的一些内容。这里讲一下看到的第一个内容,该sdk里用到的洗牌算法。

该库的地址为:https://github.com/go-zookeeper/zk

这个算法在这个sdk,主要是避免所有的zookeeper客户端在使用相同的配置文件时,拿到的ip+端口列表都是同样的顺序,大家集中的去访问一个zookeeper节点,zookeeper集群内各个节点负载很不均衡。

看看源代码

// stringShuffle performs a Fisher-Yates shuffle on a slice of strings
func stringShuffle(s []string) {
	for i := len(s) - 1; i > 0; i-- {
		j := rand.Intn(i + 1)
		s[i], s[j] = s[j], s[i]
	}
}

很简单的,说人话就是,从最后一个往前,每次都在这个之前及自身随机生成一个下标确定一个元素,交换这两个数。这里注意一点:可能出现自己与自己交换的情况,这也是保证随机性的一个重要原因,否则,就会出现一个很验证的问题,就是某个元素在随机洗牌之后,肯定不会出现在自己原来的位置,这样的话就排除了一大批可能的排列,而不是真正的随机。

在该库的使用过程中,zookeeper集群配置有三个zookeeper节点,我感觉所有客户端仍然会集中的去访问相同的zookeeper节点。

分析原因:

该库的随机算法在调用rand.Intn()函数之前,没有使用任何随机因素进行初始化,可以写代码验证,未初始化的rand.Intn()每次启动,生成的都是相同的随机数。

//测试代码
package main

import (
        "fmt"
        "math/rand"
)

func main() {
        fmt.Println(rand.Intn(30))
}
#!/bin/bash

#测试运行脚本

for i in $(seq 1 10)
do
        ./rand
        sleep 1
done

可以看到所有的输出都是相同的11。所以这个库在这里是有代码缺陷的,或者我看的版本有问题。所有的客户端仍然会集中去访问相同的节点。

腾讯云阿里云API接口签名鉴权方法

不管是腾讯云还是阿里云的API,防止篡改、重放都是基本要求,他们的接口鉴权算法也是一样的,这个鉴权的方法也是通用的。

假如有一个接口测算两个名字的缘分,比如shabia和tiancaib的缘分值为0.001,
大致就是GET qcloud.com?action=calcyf&name1=shabia&name2=tiancaib

问题来了,上面的这些参数是如何做到鉴权和防止重放的?

1.身份标识ID和KEY

在腾讯云或阿里云的后台,API接口的调用都要求先创建调用者身份标识ID和KEY。
腾讯云管它们叫SecretId和SecretKey,阿里云管它们叫AccessKeyID和AccessKeySecret,不同的马甲而已,一个东西。
身份ID和KEY是用户在云后台获取到的,并且要求用户做好保密,这意味着用户知道ID和KEY,腾讯云或者阿里云也可以根据ID查询到KEY。

2.对请求字符串进行签名

鉴权是通过使用客户端和服务端都知道的KEY对请求字符串按照相同的算法进行摘要提取,两者一致的话,鉴权成功,否则,鉴权失败。

在请求的参数中,action=calcyf是一个参数,指定调用的功能类型;name1=shabia 和name2=tiancaib都是功能实现需要的参数,不做解释。
带上id=ID,这样服务端才知道你是谁,才能找出来ID和KEY。

在action=calcyf&id=ID&name1=shabia&name2=tiancaib这个请求中,接口一般要求对请求参数进行排序后组织,
对参数进行排序,是对指定参数集构造唯一字符串的一个简单方法,腾讯云要求对参数按照参数名称的ASCII升序排列参数,因此action>id>name1>name2。

排好序后,得到的请求参数字符串就是action=calcyf&id=ID&name1=shabia&name2=tiancaib ,记为strRequest。

那么签名signature=base64(sha1(strRequest,KEY)),服务端只需要按照这个方法也算一下signature,相同则表明调用身份合法,参数在传递过程中也没有被篡改。

3.如何防止重放

在action=calcyf&id=ID&name1=shabia&name2=tiancaib&signature=xxxx这个串里,
没有时间戳信息,服务器不知道这个请求是什么时间生成的,这就意味着如果报文被捕获,就可以无限次重放,至少鉴权可以通过。
解决的办法是在请求参数中加上一个时间戳参数,这样请求串变成了:
action=calcyf&id=ID&name1=shabia&name2=tiancaib&timestamp=123456789&signature=xxxx
服务器检查时间戳参数距离现在多久了,如果超过30秒,认为请求无效,这个30秒的时间,不同云厂家可能不一样。

重放的问题还没有解决,至少在30秒内还可以重放攻击。
解决的方法也比较简单直接,就是再新增一个随机数作为参数,客户端只需要保证这个随机数在30秒内不会重复即可。这个随机数大家都叫它nonce。
加入nonce后的请求串就变成了
action=calcyf&id=ID&name1=shabia&name2=tiancaib&nonce=123456&timestamp=123456789&signature=xxxx
比如客户端使用rand生成随机整数作为nonce,那么基本可以任务30秒内不会出现重复的nonce。
服务器只需要记录下该接口该用户在30秒内用过的nonce,如果发现相同nonce在30秒时间窗口内重复出现,就判定为重放攻击。

如何实现这个30秒内的统计呢,比较简单的可以使用redis的超时机制。针对每一个请求,构造{action}{ID}{nonce}作为key的元素,值随意,以30秒有效期写入redis。
比如上面的,构造的key就是calcyf_ID_123456,剩下的大概知道的。

至于redis的性能,就不需要替大厂担心了,这种基础产品,几千万QPS都是小事。

zk 镜像和日志文件定时清理脚本

zookeeper在运行中会持续的导出镜像文件snapshot和日志文件log,对于比较写zk比较频繁的应用,snapshot文件生成频繁,很容易导致磁盘满告警。解决的方法一般有下面的三种:

1.检查zookeeper的使用是否正确。一般而言,zookeeper作为配置中心,不应该会出现先非常频繁的写。项目中有一次误用,把zookeeper作为分布式节点的心跳中转站,非常多的节点每秒一次的快速写zookeeper上报运行状态,这种用法不是很合适。用zk的临时节点做保活就可以减少zk写的次数,增长snapshot的触发时间间隔;

2.调整zoo.cfg中snapCount的值。这个值表示当log中有多少事务时,生成一次snapshot。默认为10万,把这个值改大,可以将snapshot的时间间隔变长;

3.事后清理补救。如果zookeeper负载真很高并且无法降低,就是会频繁的snapshot的话,准备脚本及时清理镜像文件,免得半夜磁盘告警幽灵电话。

提供一个清理脚本供参考。

#!/bin/bash

ReservedNum=30                      #保留文件数量
rm_file_dir='/data/zookeeper/version-2'  #需要删除文件的路径

cd $rm_file_dir    #进入文件夹
RootDir=$(cd $(dirname $0); pwd)      #当前文件路径
while true
do
	if [ $RootDir == $rm_file_dir ];then   #判断所在目录是否正确
		FileNum=$(ls -l | grep ^- | wc -l)    #查找文件数量
		OldFile=$(ls -rt *|head -1)         #找出最早文件
    		while (($FileNum>$ReservedNum))  #文件数超过设置变量才执行
    		do
   			 echo "Delete File:"$RootDir'/'$OldFile   #打印要删除的文件名称
    			 rm -f $RootDir'/'$OldFile                       #删除文件
    			 let "FileNum--"                                      #变量自减操作
    			 OldFile=$(ls -rt *|head -1)         #更新最早文件
    		done
	else
    		echo "error file path "                         #所在目录不对打印出路径错误
	fi
	echo "loop done"
	sleep 60
done

zookeeper在公网上的数据安全

在上一篇文章中提到过如何保证zookeeper在节点访问控制上的ACL实现机制。这种机制依赖于zookeeper的配置和支持,今天看了下阿里云的zookeeper引擎和相关的API,大致猜测阿里云是怎么实现zookeeper的公网访问安全。

方法很简单,就是计算机里经常说的,加一层代理。

zookeeper部署在阿里云内部网络,通过防火墙或者物理网络隔离,保证只有代理能够访问zookeeper服务。

在代理上实现一套服务,对外部以Restful接口的形式,封装了一系列对zk节点进行增删读写的接口,这些接口需要通过阿里云的公共鉴权才能调用成功。

也就是说,用阿里云的鉴权保证zookeeper读写安全。

但是这种方式也有一个没有解决的问题:就目前阿里云提供的API接口来看,zookeeper的watch机制是完全用不上了。

如果应用依赖于zookeeper的watch机制,那么可能就需要改造成轮训的方式去实现,但是这样又会有一个问题,因为代理的存在,zookeeper对调用阿里云Restful接口的客户端来说,客户端最后拿到的应答结果,不代表zookeeper真实的状态。