1、测试环境
测试环境的集群使用本机docker 容器模拟多主机环境进行搭建。
1.1 Redis集群
采用三主三从的模式进行搭建,这里由于主机资源有限,使用docker 启动六个容器模拟六台主机,分别占用端口号6371~6376。
创建文件目录
在·/usr/local/docker-redis/redis-cluster
路径下分边创建文件夹名为6371~6376的文件夹,用于存储每个容器的配置文件与data。
编写配置文件
1、编写每个 Redis 容器的配置文件
port {port} #端口从6371-6376
requirepass 1234
masterauth 1234
protected-mode no
daemonize no
appendonly yes
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 15000
cluster-announce-port {port}
cluster-announce-bus-port {port}
2、编写docker-compose文件
# 描述 Compose 文件的版本信息
version: "2.0"
services:
lockredis1: # 服务名称
image: redis # 创建容器时所需的镜像
container_name: lockredis1 # 容器名称
restart: always # 容器总是重新启动
network_mode: "host" # host 网络模式
volumes: # 数据卷,目录挂载
- /usr/local/docker-redis/redis-cluster/6371/conf/redis.conf:/usr/local/etc/redis/redis.conf
- /usr/local/docker-redis/redis-cluster/6371/data:/data
command: redis-server /usr/local/etc/redis/redis.conf # 覆盖容器启动后默认执行的命令
lockredis2:
image: redis
container_name: lockredis2
network_mode: "host"
volumes:
- /usr/local/docker-redis/redis-cluster/6372/conf/redis.conf:/usr/local/etc/redis/redis.conf
- /usr/local/docker-redis/redis-cluster/6372/data:/data
command: redis-server /usr/local/etc/redis/redis.conf
lockredis3:
image: redis
container_name: lockredis3
network_mode: "host"
volumes:
- /usr/local/docker-redis/redis-cluster/6373/conf/redis.conf:/usr/local/etc/redis/redis.conf
- /usr/local/docker-redis/redis-cluster/6373/data:/data
command: redis-server /usr/local/etc/redis/redis.conf
lockredis4:
image: redis
container_name: lockredis4
network_mode: "host"
volumes:
- /usr/local/docker-redis/redis-cluster/6374/conf/redis.conf:/usr/local/etc/redis/redis.conf
- /usr/local/docker-redis/redis-cluster/6374/data:/data
command: redis-server /usr/local/etc/redis/redis.conf
lockredis5:
image: redis
container_name: lockredis5
network_mode: "host"
volumes:
- /usr/local/docker-redis/redis-cluster/6375/conf/redis.conf:/usr/local/etc/redis/redis.conf
- /usr/local/docker-redis/redis-cluster/6375/data:/data
command: redis-server /usr/local/etc/redis/redis.conf
lockredis6:
image: redis
container_name: lockredis6
network_mode: "host"
volumes:
- /usr/local/docker-redis/redis-cluster/6376/conf/redis.conf:/usr/local/etc/redis/redis.conf
- /usr/local/docker-redis/redis-cluster/6376/data:/data
command: redis-server /usr/local/etc/redis/redis.conf
创建并启动全部容器
在docker-compose文件所在目录,执行下列命令:
docker-compose up -d
创建成功后,可以通过docker-compose ps
查看结果:
~/lincxlock/redis # docker-compose ps
NAME COMMAND SERVICE STATUS PORTS
lockredis1 "docker-entrypoint.s…" lockredis1 running
lockredis2 "docker-entrypoint.s…" lockredis2 running
lockredis3 "docker-entrypoint.s…" lockredis3 running
lockredis4 "docker-entrypoint.s…" lockredis4 running
lockredis5 "docker-entrypoint.s…" lockredis5 running
lockredis6 "docker-entrypoint.s…" lockredis6 running
启动Redis集群
进入其中一个redis容器,例如lockredis1
# docker exec -it lockredis1 bash
执行以下命令,这里的172.27.76.104
是主机的ip地址 :
>redis-cli -a 1234 --cluster create 172.27.76.104:6371 172.27.76.104:6372 172.27.76.104:6373 172.27.76.104:6374 172.27.76.104:6375 172.27.76.104:6376 --cluster-replicas 1
集群创建并启动成功:
1.2 ETCD集群
启动三个etcd容器,因为每个etcd需要两个端口,所以将机器的20000-20006端口每两个分配给一个etcd容器。
创建文件目录
在·~/etcd/
路径下分边创建文件夹名为data1、data2、data3的文件夹,用于存储每个容器的配置文件与data。
编写配置文件
编写docker-compose文件
version: '3'
networks:
etcd-net: # 网络
driver: bridge # 桥接模式
services:
locketcd1:
image: quay.io/coreos/etcd:v3.5.1 # 镜像
container_name: locketcd1 # 容器名 --name
restart: always # 总是重启
networks:
- etcd-net # 使用的网络 --network
ports: # 端口映射 -p
- "20000:2379"
- "20001:2380"
environment: # 环境变量 --env
- ALLOW_NONE_AUTHENTICATION=yes # 允许不用密码登录
- ETCD_NAME=locketcd1 # etcd 的名字
- ETCD_INITIAL_ADVERTISE_PEER_URLS=http://locketcd1:2380 # 列出这个成员的伙伴 URL 以便通告给集群的其他成员
- ETCD_LISTEN_PEER_URLS=http://0.0.0.0:2380 # 用于监听伙伴通讯的URL列表
- ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379 # 用于监听客户端通讯的URL列表
- ETCD_ADVERTISE_CLIENT_URLS=http://locketcd1:2379 # 列出这个成员的客户端URL,通告给集群中的其他成员
- ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster # 在启动期间用于 etcd 集群的初始化集群记号
- ETCD_INITIAL_CLUSTER=locketcd1=http://locketcd1:2380,locketcd2=http://locketcd2:2380,locketcd3=http://locketcd3:2380 # 为启动初始化集群配置
- ETCD_INITIAL_CLUSTER_STATE=new # 初始化集群状态
- ETCDCTL_API=3 # 升级api版本,使用最新的v3 API
volumes:
- $PWD/data1:/etcd-data # 挂载的数据卷
- /etc/localtime:/etc/localtime
locketcd2:
image: quay.io/coreos/etcd:v3.5.1
container_name: locketcd2
restart: always
networks:
- etcd-net
ports:
- "20002:2379"
- "20003:2380"
environment:
- ALLOW_NONE_AUTHENTICATION=yes
- ETCD_NAME=locketcd2
- ETCD_INITIAL_ADVERTISE_PEER_URLS=http://locketcd2:2380
- ETCD_LISTEN_PEER_URLS=http://0.0.0.0:2380
- ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379
- ETCD_ADVERTISE_CLIENT_URLS=http://locketcd2:2379
- ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster
- ETCD_INITIAL_CLUSTER=locketcd1=http://locketcd1:2380,locketcd2=http://locketcd2:2380,locketcd3=http://locketcd3:2380
- ETCD_INITIAL_CLUSTER_STATE=new
- ETCDCTL_API=3
volumes:
- $PWD/data2:/etcd-data
- /etc/localtime:/etc/localtime
locketcd3:
image: quay.io/coreos/etcd:v3.5.1
container_name: locketcd3
restart: always
networks:
- etcd-net
ports:
- "20004:2379"
- "20005:2380"
environment:
- ALLOW_NONE_AUTHENTICATION=yes
- ETCD_NAME=locketcd3
- ETCD_INITIAL_ADVERTISE_PEER_URLS=http://locketcd3:2380
- ETCD_LISTEN_PEER_URLS=http://0.0.0.0:2380
- ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379
- ETCD_ADVERTISE_CLIENT_URLS=http://locketcd3:2379
- ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster
- ETCD_INITIAL_CLUSTER=locketcd1=http://locketcd1:2380,locketcd2=http://locketcd2:2380,locketcd3=http://locketcd3:2380
- ETCD_INITIAL_CLUSTER_STATE=new
- ETCDCTL_API=3
volumes:
- $PWD/data3:/etcd-data
- /etc/localtime:/etc/localtime
创建并启动全部容器
在docker-compose文件所在目录,执行下列命令:
docker-compose up -d
创建成功后,可以通过docker-compose ps
查看结果:
1.3 Zookeeper集群
启动三个Zookeeper容器,因为每个etcd需要两个端口,所以将机器的2181-2183端口分别分配给容器。
创建文件目录
在·~/zk/
路径下分边创建文件夹名为data1、data2、data3的文件夹,用于存储每个容器的配置文件与data。
编写配置文件
编写docker-compose文件
version: '2' #版本号
services:
lockzk1: #
image: zookeeper:latest
restart: always
container_name: lockzk1
ports:
- "2181:2181"
environment:
ZOO_MY_ID: 1 #环境变量 这个id在集群必须是唯一的,该值介于1~255之间
ZOO_SERVERS: server.1=lockzk1:2888:3888 server.2=lockzk2:2888:3888 server.3=lockzk3:2888:3888 #此变量允许您指定Zookeeper集群的计算机列表;
lockzk2:
image: zookeeper:latest
restart: always
container_name: lockzk2
ports:
- "2182:2181"
environment:
ZOO_MY_ID: 2
ZOO_SERVERS: server.1=lockzk1:2888:3888 server.2=lockzk2:2888:3888 server.3=lockzk3:2888:3888
lockzk3:
image: zookeeper:latest
restart: always
container_name: lockzk3
ports:
- "2183:2181"
environment:
ZOO_MY_ID: 3
ZOO_SERVERS: server.1=lockzk1:2888:3888 server.2=lockzk2:2888:3888 server.3=lockzk3:2888:3888
创建并启动全部容器
在docker-compose文件所在目录,执行下列命令:
docker-compose up -d
创建成功后,可以通过docker-compose ps
查看结果:
2、测试内容
2.1 功能测试
2.1.1 分布式锁功能测试
编写测试代码
var(
zkserver1 = "172.27.78.70:2181"
zkserver2 = "172.27.78.70:2182"
zkserver3 = "172.27.78.70:2183"
redisserver1 = "172.27.78.70:6371"
redisserver2 = "172.27.78.70:6372"
redisserver3 ="172.27.78.70:6373"
etcdserver1 = "172.27.78.70:20000"
etcdserver1 = "172.27.78.70:20002"
etcdserver1 = "172.27.78.70:20004"
)
// 测试三种实现方式的功能
func TestCallLock() {
CallLock("redislock","lincxlock",[]string{redisserver1,redisserver2,redisserver3})
CallLock("etcdlock","lincxlock",[]string{etcdserver1,etcdserver2,etcdserver3})
CallLock("zklock","lincxlock",[]string{zkerver1,zkerver2,zkerver3})
}
// 模拟10个机器争夺分布式锁
func CallLockLock(locktype,lockey string,hosts []string){
conf,err:=config.NewLockConf(locktype,5,hosts)
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
ll ,err:= lock.NewLincxLock("locklincx",config.LockConf)
if err != nil {
log.Printf("initlock error:%v", err)
return
}
err = ll.Lock()
if err != nil {
log.Printf("number %v gorotine lock error:%v", n, err)
return
}
log.Printf("number %v gorotine get lock\n", n)
time.Sleep(1 * time.Second)
log.Printf("number %v gorotine lease lock\n", n)
//ll.Unlock()
}(i)
}
wg.Wait()
}
执行上面的代码,测试结果如下:
redis 测试结果:
etcd测试结果:
zookeeper测试结果:
2.1.2 RPC服务测试
编写测试代码
func TestRPC(){
StartServer()
time.Sleep(10*time.Second)
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(n int){
defer wg.Done()
RpcCall(n)
}(i)
}
wg.Wait()
sv.Close()
}
func StartServer(){
sv = rpcserver.NewServer("lincxlock-app",nil,re1,&rpcserver.DefaultRPCServerOption)
go func () {
defer func () {
if err:=recover();err!=nil{
log.Printf("panic err %v",err)
}
}()
sv.Serve("tcp",":8881",nil)
}()
}
func StopServer(){
sv.Close()
}
func RpcCall(n int){
log.Printf("provider len %d\n",len(re1.GetServiceList()))
op := &client.DefaultSGOption
op.AppKey = "lincxlock-app"
op.SerializeType = codec.MessagePackType
op.RequestTimeout = time.Second*10
op.DialTimeout = time.Second*10
op.FailMode = client.FailRetry
op.Retries = 3
op.Heartbeat = true
op.HeartbeatInterval = time.Second * 10
op.HeartbeatDegradeThreshold = 10
op.Registry = re1
c := client.NewSGClient(*op)
lockargs := lock.LockRPCArgs{
Locktype: "zklock",
Timeout: 5,
JobName: "/distributed-lock/lock",
Hosts: []string{zkserver},
}
reply := &lock.LockReply{}
ctx := context.Background()
err := c.Call(ctx, "LockRPC.NewLock", lockargs,reply )
if err != nil {
log.Println("err!!!" + err.Error())
return
}
time.Sleep(1*time.Second)
log.Printf("goroutine %d get lock\n",n)
unlockargs := lock.UnlockRPCArgs{
LockID: reply.LockID,
}
ulreply := &lock.UnlockReply{}
err = c.Call(ctx, "LockRPC.Unlock", unlockargs,ulreply )
if err!=nil{
log.Println("err!!"+err.Error())
return
}
log.Printf("goroutine %d unlock\n",n)
}
执行上述代码,测试结果如下:
2.1.3 Web服务测试
编写测试代码:
func TestHttp(){
httpserver.StartHttp(8888) // 启动http服务
httpserver.StartHttps(8881) //启动https服务
}
执行代码,使用postman请求
测试结果如下:
2.2 性能测试
2.2.1 加解锁性能测试
测试步骤如下:
1、为三种锁的实现分别编写用于性能测试的压测函数,内容为新建一个锁对象,调用加锁方法再调用解锁方法。
2、执行压测命令,查看压测结果,并声称。
3、通过pprof查看具体的方法执行的内存消耗与CPU运行时间。
编写的测试代码:
func BenchmarkCallZkLock(b *testing.B) {
b.ResetTimer()
for i:=0;i<b.N;i++{
CallLock("zklock","/lincxlock/lock",[]string{zkserver})
}
b.StopTimer()
}
func BenchmarkCallEtcdLock(b *testing.B) {
b.ResetTimer()
for i:=0;i<b.N;i++{
CallLock("etcdlock","/lincxlock/lock",[]string{etcdserver})
}
b.StopTimer()
}
func BenchmarkCallRedisLock(b *testing.B) {
b.ResetTimer()
for i:=0;i<b.N;i++{
CallLock("redislock","lincxlock",[]string{redisserver})
}
b.StopTimer()
}
func CallLock(locktype,lockey string,hosts []string){
conf,err:=config.NewLockConf(locktype,5,hosts)
if err!=nil{
return
}
ll ,err:= lock.NewLincxLock(lockey,conf)
if err != nil {
return
}
err = ll.Lock()
if err != nil {
return
}
ll.Unlock()
}
redis测试结果
基于Redis加解锁测试结果如下所示,结果显示执行363次,每次消耗的平均时间为4.356796毫秒,每次分配的内存大小为22566 字节(22.56KB)以及调用分配了107次内存。其中图显示,内存占用比较多的方法是获取Redis连接的方法。
etcd测试结果
基于ETCD的分布式锁测试结果如图所示,结果显示执行159次,每次消耗的平均时间为148.108404毫秒,每次分配的内存大小为631627字节(631.627KB)以及调用分配了1192次内存。其中图显示,内存占用比较大的是ETCD客户端的日志模块。
Zookeeper结果
ZK的测试结果如图所示,结果显示执行165次,每次消耗的平均时间为132.909104毫秒,平均每次分配的内存大小为3154434字节(3.1M)以及调用分配了147次内存。其中图显示,Zookeeper客户端的连接与接收数据模块占的内存比较大。
2.2.2 RPC性能测试
LINCXRPC框架的性能直接影响了LINCXLOCK的RPC服务,所以对RPC的Call方法进行压测,首先启动服务端,再使用LINCXRPC的Client的Call方法调用服务。
编写测试代码
func BenchmarkMakeCallMSGP(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
MakeCall(codec.MessagePackType)
}
b.StopTimer()
}
func MakeCallCall(codec.MessagePackType){
op := &client.DefaultSGOption
op.AppKey = "lincxlock-app"
op.SerializeType = codec.MessagePackType
op.RequestTimeout = time.Second*10
op.DialTimeout = time.Second*10
op.FailMode = client.FailRetry
op.Retries = 3
op.Heartbeat = true
op.HeartbeatInterval = time.Second * 10
op.HeartbeatDegradeThreshold = 10
op.Registry = re1
c := client.NewSGClient(*op)
lockargs := lock.LockRPCArgs{
Locktype: "zklock",
Timeout: 5,
JobName: "/distributed-lock/lock",
Hosts: []string{zkserver},
}
reply := &lock.LockReply{}
ctx := context.Background()
err := c.Call(ctx, "LockRPC.NewLock", lockargs,reply )
if err != nil {
log.Println("err!!!" + err.Error())
return
}
time.Sleep(1*time.Second)
log.Printf("goroutine %d get lock\n",n)
unlockargs := lock.UnlockRPCArgs{
LockID: reply.LockID,
}
ulreply := &lock.UnlockReply{}
err = c.Call(ctx, "LockRPC.Unlock", unlockargs,ulreply )
if err!=nil{
log.Println("err!!"+err.Error())
return
}
log.Printf("goroutine %d unlock\n",n)
}
每次消耗的平均时间为3.084287毫秒,每次分配的内存大小为12390字节(12.390KB)以及调用分配了153次内存。
通过分析内存分配情况,如图1所示,可知主要消耗内存的模块是RPC客户端、选择器(负载均衡)。由图2可知,耗时较多的部分是网络通信以及系统调用,系统调用耗时主要与服务端的反射有关。总体来说,LINCXRPC性能表现较好,适用于LINCXLOCK作为RPC框架。
3、总结
该文档主要介绍了LINCXLOCK的LINCXLOCK测试的内容,包括搭建三种集群的测试环境,对LINCXLOCK的核心三大板块进行功能测试,最后对加解锁方法与RPC的Call方法使用go语言自带的测试工具进行性能测试。