q里Zwhoami
C服务Q部|?个实例,分别一一验证各种cd的K8S Service服务范畴?/p>
大致逐一从下面列表逐一验证每种cd的Service讉K方式Q?/p>
Service Name
CLUSTER-IP
EXTERNAL-IP
一些设定如下:
v1.27.3
10.0.1.0/24
先部|包?个实例的whoami
Q?/p>
# cat << 'EOF' | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: whoami
labels:
app: whoami
spec:
replicas: 3
selector:
matchLabels:
app: whoami
template:
metadata:
labels:
app: whoami
spec:
containers:
- name: whoami
image: containous/whoami
ports:
- containerPort: 80
name: web
EOF
查看一下:
# kubectl get all
NAME READY STATUS RESTARTS AGE
pod/whoami-767d459f67-qffqw 1/1 Running 0 23m
pod/whoami-767d459f67-xdv9p 1/1 Running 0 23m
pod/whoami-767d459f67-gwpgx 1/1 Running 0 23m
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/whoami 3/3 3 3 23m
NAME DESIRED CURRENT READY AGE
replicaset.apps/whoami-767d459f67 3 3 3 23m
安装一个包含有curl
的busybox方便后箋调试Q?/p>
kubectl run busybox-curl --image=yauritux/busybox-curl --command -- sleep 3600
另v一个终端,输入下面命oq入Q?/p>
kubectl exec -ti busybox-curl -n default -- sh
环境准备好之后,下面逐一试各种cdQ?/p>
K8S默认Service?code>Cluster IP模式Q面向内部Pod以及通过Ingress对外提供服务?/p>
下面一张图很清晰解释清楚了Port
?code>TargetPort适用情景Q?code>Port为Service对外输出的端口,TargetPort
为服务后端Pod的端口,两者之间有一个{换:port -> targetPort -> containerPort
?br />
创徏一个ServiceQ?/p>
cat << 'EOF' | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
labels:
name: whoami-clusterip
name: whoami-clusterip
spec:
ports:
- port: 80
targetPort: 80
protocol: TCP
selector:
app: whoami
EOF
部v后可以查看一下:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/whoami-clusterip ClusterIP 10.43.247.74 <none> 80/TCP 57s
下面需要逐一试了?/p>
域名形式Q?/p>
# curl whoami-clusterip
Hostname: whoami-767d459f67-gwpgx
IP: 127.0.0.1
IP: 10.42.8.35
RemoteAddr: 10.42.9.32:35968
GET / HTTP/1.1
Host: whoami-clusterip
User-Agent: curl/7.81.0
Accept: */*
Cluster IP形式Q?/p>
# curl 10.43.247.74
Hostname: whoami-767d459f67-qffqw
IP: 127.0.0.1
IP: 10.42.3.73
RemoteAddr: 10.42.9.32:42398
GET / HTTP/1.1
Host: 10.43.247.74
User-Agent: curl/7.81.0
Accept: */*
域名解析Q只解析到Cluster IP上:
# nslookup whoami-clusterip
Server: 10.43.0.10
Address: 10.43.0.10:53
Name: whoami-clusterip.default.svc.cluster.local
Address: 10.43.247.74
原理同Cluster IP模式Qؓ指定服务l定一个额外的一个IP地址。当l端讉K该IP地址Q将量一栯{发到Service?/p>
当访?code>external IPQ其端口转换q程Q?code>port -> targetPort -> containerPort?/p>
与默认Service相比Q端口{换流E没有增加,但好处对外暴露了一个可讉K的IP地址Q不q可能需要在交换?路由器层面提供动静态\由支持?/p>
cat << 'EOF' | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
labels:
name: whoami-externalip
name: whoami-externalip
spec:
ports:
- port: 80
targetPort: 80
protocol: TCP
selector:
app: whoami
externalIPs:
- 10.10.10.10
EOF
服务昄如下Q绑定了指定的扩展IP地址10.10.10.10
?/p>
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/whoami-externalip ClusterIP 10.43.192.118 10.10.10.10 80/TCP 57s
kube-proxy
在每一个Node节点?code>10.10.10.10上徏立一个{发规则,该IP地址?code>80端口直接{发到对应的后端三?code>whoami Pod 上?/p>
-A KUBE-SERVICES -d 10.10.10.10/32 -p tcp -m comment --comment "default/whoami-externalip external IP" -m tcp --dport 80 -j KUBE-EXT-QN5HIEVYUPDP6UNK
......
-A KUBE-EXT-QN5HIEVYUPDP6UNK -j KUBE-SVC-QN5HIEVYUPDP6UNK
......
-A KUBE-SVC-QN5HIEVYUPDP6UNK ! -s 10.42.0.0/16 -d 10.43.192.118/32 -p tcp -m comment --comment "default/whoami-externalip cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ
-A KUBE-SVC-QN5HIEVYUPDP6UNK -m comment --comment "default/whoami-externalip -> 10.42.2.79:80" -m statistic --mode random --probability 0.33333333349 -j KUBE-SEP-JSAT6D2KFCSF4YLF
-A KUBE-SVC-QN5HIEVYUPDP6UNK -m comment --comment "default/whoami-externalip -> 10.42.3.77:80" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-2R66UI3G2AY2IMNM
-A KUBE-SVC-QN5HIEVYUPDP6UNK -m comment --comment "default/whoami-externalip -> 10.42.8.42:80" -j KUBE-SEP-ZHHIL2SAN2G37GCM
讉K域名Q?/p>
# curl whoami-externalip
Hostname: whoami-767d459f67-gwpgx
IP: 127.0.0.1
IP: 10.42.8.35
RemoteAddr: 10.42.9.32:46746
GET / HTTP/1.1
Host: whoami-externalip
User-Agent: curl/7.81.0
Accept: */*
讉KClusterIP形式Q?/p>
# curl 10.43.192.118
Hostname: whoami-767d459f67-qffqw
IP: 127.0.0.1
IP: 10.42.3.73
RemoteAddr: 10.42.9.32:47516
GET / HTTP/1.1
Host: 10.43.192.118
User-Agent: curl/7.81.0
Accept: */*
讉K暴露的External IPQ?/p>
# curl 10.10.10.10
Hostname: whoami-767d459f67-gwpgx
IP: 127.0.0.1
IP: 10.42.8.35
RemoteAddr: 10.42.9.0:38477
GET / HTTP/1.1
Host: 10.10.10.10
User-Agent: curl/7.81.0
Accept: */*
域名解析l果只解析到其对应的Cluster IPQ?/p>
# nslookup whoami-externalip
Server: 10.43.0.10
Address: 10.43.0.10:53
Name: whoami-externalip.default.svc.cluster.local
Address: 10.43.192.118
?code>Cluster IP相比Q多了一?code>nodePortQ这个NodePort会在K8S所有Node节点上都会开放?/p>
q里有一个端口{换过E:nodePort -> port -> targetPort -> containerPort
Q多了一层数据{换过E?/p>
服务定义如下Q?/p>
cat << 'EOF' | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
labels:
name: whoami-nodeport
name: whoami-nodeport
spec:
type: NodePort
ports:
- port: 80
targetPort: 80
nodePort: 30080
protocol: TCP
selector:
app: whoami
EOF
查看一下服务分配地址Q?/p>
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/whoami-nodeport NodePort 10.43.215.233 <none> 80:30080/TCP 57s
讉K域名Q?/p>
# curl whoami-nodeport
Hostname: whoami-767d459f67-xdv9p
IP: 127.0.0.1
IP: 10.42.2.75
RemoteAddr: 10.42.9.32:36878
GET / HTTP/1.1
Host: whoami-nodeport
User-Agent: curl/7.81.0
Accept: */*
试 CLUSTER IP Q?/p>
# curl 10.43.215.233
Hostname: whoami-767d459f67-qffqw
IP: 127.0.0.1
IP: 10.42.3.73
RemoteAddr: 10.42.9.32:40552
GET / HTTP/1.1
Host: 10.43.215.233
User-Agent: curl/7.81.0
Accept: */*
因ؓ是在每一个K8S Node节点上都会开放一?code>30080端口Q因此可以这栯?{Node IP}:{nodePort}
Q如下Node IP地址?code>10.0.1.11
# curl 10.0.1.11:30080
Hostname: whoami-767d459f67-qffqw
IP: 127.0.0.1
IP: 10.42.3.73
RemoteAddr: 10.42.1.0:1880
GET / HTTP/1.1
Host: 10.0.1.11:30080
User-Agent: curl/7.81.0
Accept: */*
域名q是只解析到对应Cluster IPQ?/p>
# nslookup whoami-nodeport
Server: 10.43.0.10
Address: 10.43.0.10:53
Name: whoami-nodeport.default.svc.cluster.local
Address: 10.43.215.233
LoadBalancer
模式Q会强制K8S Service自动开?code>nodePort?/p>
q里有一张图Q详l解析数据流向?/p>
服务数据端口转换q程Q?code>port -> nodePort -> port -> targetPort -> containerPortQ?/p>
具体服务定义Q?/p>
cat << 'EOF' | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
labels:
name: whoami-clusterip-none
name: whoami-clusterip-none
spec:
clusterIP: None
ports:
- port: 80
targetPort: 80
protocol: TCP
selector:
app: whoami
EOF
查看一下部|结果:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/whoami-loadbalancer LoadBalancer 10.43.63.92 <pending> 80:30906/TCP 57s
服务域名形式Q?/p>
# curl whoami-loadbalancer
Hostname: whoami-767d459f67-qffqw
IP: 127.0.0.1
IP: 10.42.3.73
RemoteAddr: 10.42.9.32:57844
GET / HTTP/1.1
Host: whoami-loadbalancer
User-Agent: curl/7.81.0
Accept: */*
试 CLUSTER-IP
# curl 10.43.63.92
Hostname: whoami-767d459f67-xdv9p
IP: 127.0.0.1
IP: 10.42.2.75
RemoteAddr: 10.42.9.32:42400
GET / HTTP/1.1
Host: 10.43.63.92
User-Agent: curl/7.81.0
Accept: */*
域名解析到Cluster IPQ?/p>
# nslookup whoami-loadbalancer
Server: 10.43.0.10
Address: 10.43.0.10:53
Name: whoami-loadbalancer.default.svc.cluster.local
Address: 10.43.63.92
此时whoami-loadbalancer
服务对应?code>EXTERNAL-IP ?<pending>
Q我们需要安装一个负载均衡器Q可以选择MetalLB
作ؓ负蝲均衡器?/p>
# kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.13.11/config/manifests/metallb-native.yaml
E后分配可用的LoadBalaner可分配的地址池:
cat << 'EOF' | kubectl apply -f -
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: default-pool
namespace: metallb-system
spec:
addresses:
- 10.0.1.100-10.0.1.200
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: default
namespace: metallb-system
spec:
ipAddressPools:
- default-pool
EOF
{安装完成之后,可以看到服务whoami-loadbalancer
分配的IP地址?10.0.1.101
Q?/p>
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
......
service/whoami-loadbalancer LoadBalancer 10.43.63.92 10.0.1.101 80:30906/TCP 27h
......
试一下:
# curl 10.0.1.101
Hostname: whoami-767d459f67-xdv9p
IP: 127.0.0.1
IP: 10.42.2.78
RemoteAddr: 10.42.8.0:33658
GET / HTTP/1.1
Host: 10.0.1.101
User-Agent: curl/7.79.1
Accept: */*
我们看到该服务分配的端口?code>80:30906/TCPQ?code>30906为K8S服务自动生成的NodePortcd端口?/p>
可以找Q一K8S Node节点IP地址试一下:
# curl 10.0.1.12:30906
Hostname: whoami-767d459f67-qffqw
IP: 127.0.0.1
IP: 10.42.3.77
RemoteAddr: 10.42.2.0:9717
GET / HTTP/1.1
Host: 10.0.1.12:30906
User-Agent: curl/7.81.0
Accept: */*
分析一下\pQ可以分析到该负载均衡的External_IP:80
的打量?code>NodePort:30906上,然后走Service对应{Pod:80}
量分发逻辑?/p>
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/whoami-loadbalancer" -m tcp --dport 30906 -j KUBE-EXT-NBTYBEEXACZI7DPC
......
-A KUBE-SERVICES -d 10.0.1.101/32 -p tcp -m comment --comment "default/whoami-loadbalancer loadbalancer IP" -m tcp --dport 80 -j KUBE-EXT-NBTYBEEXACZI7DPC
......
-A KUBE-EXT-NBTYBEEXACZI7DPC -m comment --comment "masquerade traffic for default/whoami-loadbalancer external destinations" -j KUBE-MARK-MASQ
-A KUBE-EXT-NBTYBEEXACZI7DPC -j KUBE-SVC-NBTYBEEXACZI7DPC
......
-A KUBE-SVC-NBTYBEEXACZI7DPC ! -s 10.42.0.0/16 -d 10.43.63.92/32 -p tcp -m comment --comment "default/whoami-loadbalancer cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ
-A KUBE-SVC-NBTYBEEXACZI7DPC -m comment --comment "default/whoami-loadbalancer -> 10.42.2.79:80" -m statistic --mode random --probability 0.33333333349 -j KUBE-SEP-E3K3SUYNFWT2VICE
-A KUBE-SVC-NBTYBEEXACZI7DPC -m comment --comment "default/whoami-loadbalancer -> 10.42.3.77:80" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-HG5MYVVID7GJOZA7
-A KUBE-SVC-NBTYBEEXACZI7DPC -m comment --comment "default/whoami-loadbalancer -> 10.42.8.42:80" -j KUBE-SEP-GFJH72YCBKBFB6OG
一般应用在有状态的服务Q或需要终端调用者自己实现负载均衡,{一些特定场景?/p>
通过调用者从端口角度分析Q数据{换流E:targetPort -> containerPort
?/p>
在意服务性能的场景,不妨试试无头模式?/p>
服务定义Q?/p>
cat << 'EOF' | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
labels:
name: whoami-clusterip-none
name: whoami-clusterip-none
spec:
clusterIP: None
ports:
- port: 80
targetPort: 80
protocol: TCP
selector:
app: whoami
EOF
查看服务部v情况Q?/p>
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/whoami-clusterip-none ClusterIP None <none> 80/TCP 9h
通过service域名讉KQK8S会自动根据服务域?code>whoami-clusterip-noneq行pick后端对应Pod IP地址?/p>
# curl whoami-clusterip-none
Hostname: whoami-767d459f67-xdv9p
IP: 127.0.0.1
IP: 10.42.2.75
RemoteAddr: 10.42.9.32:34998
GET / HTTP/1.1
Host: whoami-clusterip-none
User-Agent: curl/7.81.0
Accept: */*
查询DNS会把所有节炚w列出来?/p>
# nslookup whoami-clusterip-none
Server: 10.43.0.10
Address: 10.43.0.10:53
Name: whoami-clusterip-none.default.svc.cluster.local
Address: 10.42.3.73
Name: whoami-clusterip-none.default.svc.cluster.local
Address: 10.42.2.75
Name: whoami-clusterip-none.default.svc.cluster.local
Address: 10.42.8.35
用于引进带域名的外部服务Q这里引入内部服务作为测试?/p>
多了一层域名解析过E,端口转换程依赖于所引入服务的服务设定?/p>
服务定义Q?/p>
cat << 'EOF' | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
labels:
name: whoami-externalname
name: whoami-externalname
spec:
type: ExternalName
externalName: whoami-clusterip.default.svc.cluster.local
EOF
q里外联的是whoami-clusterip
服务的完整访问域名?/p>
查看服务部v情况Q?/p>
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/whoami-externalname ExternalName <none> whoami-clusterip.default <none> 9h
Ҏ域名讉K试Q?/p>
# curl whoami-externalname
Hostname: whoami-767d459f67-qffqw
IP: 127.0.0.1
IP: 10.42.3.77
RemoteAddr: 10.42.9.35:36756
GET / HTTP/1.1
Host: whoami-externalname
User-Agent: curl/7.81.0
Accept: */*
DNS解析l果Q?/p>
# nslookup whoami-externalname
Server: 10.43.0.10
Address: 10.43.0.10:53
whoami-externalname.default.svc.cluster.local canonical name = whoami-clusterip.default.svc.cluster.local
Name: whoami-clusterip.default.svc.cluster.local
Address: 10.43.247.74
要分析了各种cdService定义、服务引用场景以及测试流E等Q整理清楚了Q也方便在具体业务场景中q行抉择选择具体服务cd?/p>
NFS
Q这里记录一下操作记录?/p>
列出当前StorageClass
Q?/p>
kubectl get sc
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
local-path (default) rancher.io/local-path Delete WaitForFirstConsumer false 17d
nfs cluster.local/nfs-nfs-subdir-external-provisioner Delete Immediate true 6d14h
首先Q将默认的名UCؓlocal-path
修改?code>falseQ?/p>
kubectl patch storageclass local-path -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'
然后Q将nfs
讄为默认:
kubectl patch storageclass nfs -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
操作完成之后Q校验一下,可以看到已经成功?code>nfs讄为默认的StorageClass
选项?/p>
kubectl get sc
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
local-path rancher.io/local-path Delete WaitForFirstConsumer false 17d
nfs (default) cluster.local/nfs-nfs-subdir-external-provisioner Delete Immediate true 6d14h
RefQ?a >https://kubernetes.io/docs/tasks/administer-cluster/change-default-storage-class/
参与开源不是ؓ了证明什么,而是Z更好的配合工作。开源和工作在绝大部分时_都是可以和谐共处Q互怿q,Win-WIn双赢?/p>
本文内容记录了ؓ apisix 目提交的一ơpull request提交 Q访问地址Q?a >https://github.com/apache/apisix/pull/3615 Q完整过E,提交内容Z个独立的服务发现模块Q本文目的是为团队的其他同学参与C目分n的行为提供一个简单可遵@、可操作模型?
概括来讲Q简要操作流E如下:
下面为每一步具体操作的水账?/p>
提前预警Q图多费量Q慎?:))
作ؓNginx用户Q我们实际场景?Nginx Upsync 模块Q结合Consul KV作ؓ服务注册和发现Ş式?/p>
我们ZApisix构徏HTTP API服务|关Q没有发现现成的Consul KV形式服务发现模块Q既然实际业务需要,我们需要把它按照接口规范开发出来,以适应我们自己的实际场景?/p>
当服务发现模块功能开发出来后Q也是仅仅能满基本需求,q不够完善,但这时改q的思\q不是非常清楚,
既然开源社Z有类似的需求,那我们可以考虑分n开源出去,接收整个C的考验Q大家一hq?/p>
限于日常思维角度的局限,若是仅仅满工作需要,那么开源出M让你的代码接受到CҎ面面的审核,其是针对代码风根{功能、执行等有严D求的apisix目。摆正心态,接受代码评审q调_最l结果无疑是让代码更加健壮,好事一桩嘛?/p>
当然开源出M后,该模块的变更以及优化{行为就完全归属整个C了,策力Q是一U比较期待的演进方式?/p>
一个优U的开源项目,ZE_健康发展Q一般会提供邮gl方便社区参与者咨询、沟通协调等?/p>
一般来_Github会提?code>issues列表方便目使用者提交BUGQ若我们惛_C中表达意图、观点等Q就不如发在C邮gl中Q这栯够得到更多的x。比如,我们想给C׃n一个完整的服务发现模块Q就可以直接在邮件组中描q大致功能,以及大致处理程{,让社区知道我们的真实意图?/p>
Apisix开发邮件组地址为:dev@apisix.apache.org
Q但一般的邮gl都需要注意如下事:
无法传递图?/p>
下面是我发送的邮g截图Q?/p>
因ؓapache邮gl不支持富文本和囄Q实际看到的效果没有那么好看了Q下面的q接包含了该讨论完整的回复内容:
不方便打开的话Q下面提供完整邮件讨论截图,很长的截图,呵呵Q?br/>
MQ断断箋l经q三周时间的讨论Q这个过E需要有些耐心。发完邮件等有了U极反馈Q下面就可以着手准备提交代码了?/p>
?https://github.com/apache/apisix Fork到自׃库中Q然后克隆到自己工作机来?/p>
注意Q需要时M持和d保持一_
git remote add upstream https://github.com/apache/apisix.git
下面是动手开q了?/p>
Consul KV服务发现模块文g?consul_kv.lua
Q相对位|ؓQ?code>apisix/discovery/consul_kv.lua。我们想提交到项目主qԌ那么代码必遵循已有规范?/p>
针对apisix
的服务发C码,需要有配置,必ȝZ套完整的服务配置 schema
定义Q如下?/p>
local schema = {
type = "object",
properties = {
servers = {
type = "array",
minItems = 1,
items = {
type = "string",
}
},
fetch_interval = {type = "integer", minimum = 1, default = 3},
keepalive = {
type = "boolean",
default = true
},
prefix = {type = "string", default = "upstreams"},
weight = {type = "integer", minimum = 1, default = 1},
timeout = {
type = "object",
properties = {
connect = {type = "integer", minimum = 1, default = 2000},
read = {type = "integer", minimum = 1, default = 2000},
wait = {type = "integer", minimum = 1, default = 60}
},
default = {
connect = 2000,
read = 2000,
wait = 60,
}
},
skip_keys = {
type = "array",
minItems = 1,
items = {
type = "string",
}
},
default_service = {
type = "object",
properties = {
host = {type = "string"},
port = {type = "integer"},
metadata = {
type = "object",
properties = {
fail_timeout = {type = "integer", default = 1},
weigth = {type = "integer", default = 1},
max_fails = {type = "integer", default = 1}
},
default = {
fail_timeout = 1,
weigth = 1,
max_fails = 1
}
}
}
}
},
required = {"servers"}
}
当然Q你需要区分每一个配|项是不是必填项Q非必传w要具有默认|以及上限或下限约束等?/p>
下面需要在该模块启动时q行用户配|是否错误,无法兼容、恢复错误的话,需要直接用Lua内置错误日志接口输出Q?/p>
error("Errr MSG")
另外Q若要引?resty.worker.events
lgQ不要提?code>requireQ比如在文g头部提前声明Ӟ
loca events = require("resty.worker.events")
启动后,有可能在日志文件中出现如下异常Q?/p>
2021/02/23 02:32:20 [error] 7#7: init_worker_by_lua error: /usr/local/share/lua/5.1/resty/worker/events.lua:175: attempt to index local 'handler_list' (a nil value)
stack traceback:
/usr/local/share/lua/5.1/resty/worker/events.lua:175: in function 'do_handlerlist'
/usr/local/share/lua/5.1/resty/worker/events.lua:215: in function 'do_event_json'
/usr/local/share/lua/5.1/resty/worker/events.lua:361: in function 'post'
/usr/local/share/lua/5.1/resty/worker/events.lua:614: in function 'configure'
/usr/local/apisix/apisix/init.lua:94: in function 'http_init_worker'
init_worker_by_lua:5: in main chunk
推荐做法是gq加载,在该模块被加载时q行引用?/p>
local events
local events_list
......
function _M.init_worker()
......
events = require("resty.worker.events")
events_list = events.event_list(
"discovery_consul_update_application",
"updating"
)
if 0 ~= ngx.worker.id() then
events.register(discovery_consul_callback, events_list._source, events_list.updating)
return
end
......
end
单元试代码的执行,会在你提交PR代码后自动执行持l集成行为内执行?/p>
首先Q需要本机执行单元测试前Q需要提前准备好所需Docker试实例Q?/p>
docker run --rm --name consul_1 -d -p 8500:8500 consul:1.7 consul agent -server -bootstrap-expect=1 -client 0.0.0.0 -log-level info -data-dir=/consul/data
docker run --rm --name consul_2 -d -p 8600:8500 consul:1.7 consul agent -server -bootstrap-expect=1 -client 0.0.0.0 -log-level info -data-dir=/consul/data
docker run --rm -d \
-e ETCD_ENABLE_V2=true \
-e ALLOW_NONE_AUTHENTICATION=yes \
-e ETCD_ADVERTISE_CLIENT_URLS=http://0.0.0.0:2379 \
-e ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379 \
-p 2379:2379 \
registry.api.weibo.com/wesync/wbgw/etcd:3.4.9
然后Q安装项目依赖:
make deps
其次Q别忘记在apisix目持箋集成脚本相应位置d相应依赖?/p>
比如Q因为单元测试依赖于端口分别?500?600的两个Consul Server实例Q需要在执行单元试之前提前q行Q因此你需要在对应的持l集成文件上d所需q行实例。比如其中一个位|:
仅仅提供服务发现consul_kv.lua
q一个文Ӟ是无法被仓库理员采U的Q因为除了你自己以外Q别人无法确定你提交的代码所提供功能是否_让h信服Q除非你能提供较为完整的 Test::Nginx
单元试支持Q自我证明?/p>
Test::Nginx
单元试可能针对很多人来Ԍ是一个拦路虎Q但其实有些耐心Q你会发现它的美妙之处?/p>
单入门可参?https://time.geekbang.org/column/article/109506 Q若只需要学习单元测试,其实不需要购买整个专辑的Q。在使用q程中需要参考在U文档:https://metacpan.org/pod/Test::Nginx::Socket Q需要一些耐心p一Ҏ间慢慢消化?/p>
如何q行Nginx单元试案例Q具体参看:
https://github.com/apache/apisix/blob/master/doc/zh-cn/how-to-build.md
至于Apisix定制部分单元试部分Q可以直接参考已有的单元试文g卛_?/p>
Consul KV服务发现的单元测试模块相对\?t/discovery/consul_kv.lua
Q在U地址为: https://github.com/apache/apisix/blob/master/t/discovery/consul_kv.t 。该文g大约500多行Q比真正的模?code>consul_kv.lua代码行数q多。但比较完整覆盖了所能想到的所有场景,虽然写v来虽然有些麻烦,但针对应用到U上大量业务的核心代码,无论多认真和谨慎都是不ؓq的?/p>
以往针对关键核心模块的每一ơP代,心里面大概有些忐忑七上八下吧Q也不太敢直接应用到U上。现在有了单元测试各U场景的覆盖辅助验证q代变更效果Q自信心是有了,也可以给别h拍着胸脯保证修改没问题。当然若后箋发现隐藏的问题,直接d上对应的单元试覆盖上即可?/p>
我们q次只提供一个服务发现模块,因此只需要单独测?code>consul_kv.t文g卛_Q?/p>
# prove -Itest-nginx/lib -I./ t/discovery/consul_kv.t
......
t/discovery/consul_kv.t .. ok
All tests successful.
Files=1, Tests=102, 36 wallclock secs ( 0.05 usr 0.01 sys + 0.78 cusr 0.41 csys = 1.25 CPU)
Result: PASS
出现试案例p|问题Q可以去 apisix/t/servroot/logs
路径下查?error.log
文g暴露出的异常{问题?/p>
有些一些测试用例需要组合一l较为复杂的使用场景Q比如我们准备一l后端节点:
server 1
server 2
server 3
server 4
q些节点被频繁执行注册Consul节点然后再解除注册若q@环过E:清理注册 -> 注册 -> 解除注册 -> 注册 -> 解除注册 -> 注册 -> 解除注册 -> 注册
Q目的检验已解除注册的失效节Ҏ否还会存在内存中{?/p>
有些操作Q比如注册或解除注册节点q些操作Q网关的consul_kv.lua
服务模块在物理层面需要wait一Ҏ间等待网x化这些变化,因此我们需要额外提供一?/sleep
接口Q请求时需要故意休眠几U钟旉{待下一ơ请求生效?/p>
=== TEST 7: test register & unregister nodes
--- yaml_config eval: $::yaml_config
--- apisix_yaml
routes:
-
uri: /*
upstream:
service_name: http://127.0.0.1:8500/v1/kv/upstreams/webpages/
discovery_type: consul_kv
type: roundrobin
#END
--- config
location /v1/kv {
proxy_pass http://127.0.0.1:8500;
}
location /sleep {
content_by_lua_block {
local args = ngx.req.get_uri_args()
local sec = args.sec or "2"
ngx.sleep(tonumber(sec))
ngx.say("ok")
}
}
--- timeout: 6
--- request eval
[
"DELETE /v1/kv/upstreams/webpages/?recurse=true",
"PUT /v1/kv/upstreams/webpages/127.0.0.1:30511\n" . "{\"weight\": 1, \"max_fails\": 2, \"fail_timeout\": 1}",
"GET /sleep?sec=5",
"GET /hello",
"PUT /v1/kv/upstreams/webpages/127.0.0.1:30512\n" . "{\"weight\": 1, \"max_fails\": 2, \"fail_timeout\": 1}",
"GET /sleep",
"GET /hello",
"GET /hello",
"DELETE /v1/kv/upstreams/webpages/127.0.0.1:30511",
"DELETE /v1/kv/upstreams/webpages/127.0.0.1:30512",
"PUT /v1/kv/upstreams/webpages/127.0.0.1:30513\n" . "{\"weight\": 1, \"max_fails\": 2, \"fail_timeout\": 1}",
"PUT /v1/kv/upstreams/webpages/127.0.0.1:30514\n" . "{\"weight\": 1, \"max_fails\": 2, \"fail_timeout\": 1}",
"GET /sleep",
"GET /hello?random1",
"GET /hello?random2",
"GET /hello?random3",
"GET /hello?random4",
"DELETE /v1/kv/upstreams/webpages/127.0.0.1:30513",
"DELETE /v1/kv/upstreams/webpages/127.0.0.1:30514",
"PUT /v1/kv/upstreams/webpages/127.0.0.1:30511\n" . "{\"weight\": 1, \"max_fails\": 2, \"fail_timeout\": 1}",
"PUT /v1/kv/upstreams/webpages/127.0.0.1:30512\n" . "{\"weight\": 1, \"max_fails\": 2, \"fail_timeout\": 1}",
"GET /sleep?sec=5",
"GET /hello?random1",
"GET /hello?random2",
"GET /hello?random3",
"GET /hello?random4",
]
--- response_body_like eval
[
qr/true/,
qr/true/,
qr/ok\n/,
qr/server 1\n/,
qr/true/,
qr/ok\n/,
qr/server [1-2]\n/,
qr/server [1-2]\n/,
qr/true/,
qr/true/,
qr/true/,
qr/true/,
qr/ok\n/,
qr/server [3-4]\n/,
qr/server [3-4]\n/,
qr/server [3-4]\n/,
qr/server [3-4]\n/,
qr/true/,
qr/true/,
qr/true/,
qr/true/,
qr/ok\n/,
qr/server [1-2]\n/,
qr/server [1-2]\n/,
qr/server [1-2]\n/,
qr/server [1-2]\n/
]
除了代码能够正常q{Q我们还需要准备相应的Markdown文档辅助说明如何使用我们的模块,帮助C用户更好使用它?/p>
C一般以英文文档为先Q?只有在精力满的情况下,可以补充中文文档?/p>
下面是要准备Markdown文档了,其文档\径ؓQ?code>doc/discovery/consul_kv.mdQ单独的文档需要在其它已有文档挂接上对应链接,方便索引?/p>
文档路径为:doc/discovery/consul_kv.md
Q在U地址Q?a >https://github.com/apache/apisix/blob/master/docs/en/latest/discovery/consul_kv.md
一般徏议需要在文档中能够清楚说明模块的使用方式Q以及注意事,其是配|参C用方式等。比如下面的配置说明:
```yaml
discovery:
consul_kv:
servers:
- "http://127.0.0.1:8500"
- "http://127.0.0.1:8600"
prefix: "upstreams"
skip_keys: # if you need to skip special keys
- "upstreams/unused_api/"
timeout:
connect: 1000 # default 2000 ms
read: 1000 # default 2000 ms
wait: 60 # default 60 sec
weight: 1 # default 1
fetch_interval: 5 # default 3 sec, only take effect for keepalive: false way
keepalive: true # default true, use the long pull way to query consul servers
default_server: # you can define default server when missing hit
host: "127.0.0.1"
port: 20999
metadata:
fail_timeout: 1 # default 1 ms
weight: 1 # default 1
max_fails: 1 # default 1
```
......
The `keepalive` has two optional values:
- `true`, default and recommend value, use the long pull way to query consul servers
- `false`, not recommend, it would use the short pull way to query consul servers, then you can set the `fetch_interval` for fetch interval
每一个文档都不应该成Z息孤岛,它需要在其它文档上挂载上一个连接地址Q因此我们需要在合适的地方Q比如需要在 doc/discovery.md
最下面d链接地址描述Q?/p>
## Discovery modules
- eureka
- [Consul KV](discovery/consul_kv.md)
模块代码Q测试文Ӟ以及文档{准备好了之后,下面是准备提交代码到自׃库?/p>
所有内容准备好之后Q徏议执?make lint
?make license-check
两个命o代码、markdown文档{是否满项目规范要求?/p>
# make lint
./utils/check-lua-code-style.sh
+ luacheck -q apisix t/lib
Total: 0 warnings / 0 errors in 133 files
+ find apisix -name '*.lua' '!' -wholename apisix/cli/ngx_tpl.lua -exec ./utils/lj-releng '{}' +
+ grep -E 'ERROR.*.lua:' /tmp/check.log
+ true
+ '[' -s /tmp/error.log ']'
./utils/check-test-code-style.sh
+ find t -name '*.t' -exec grep -E '\-\-\-\s+(SKIP|ONLY|LAST)$' '{}' +
+ true
+ '[' -s /tmp/error.log ']'
+ find t -name '*.t' -exec ./utils/reindex '{}' +
+ grep done. /tmp/check.log
+ true
+ '[' -s /tmp/error.log ']'
# make license-check
.travis/openwhisk-utilities/scancode/scanCode.py --config .travis/ASF-Release.cfg ./
Reading configuration file [.travis/ASF-Release.cfg]...
Scanning files starting at [./]...
All checks passed.
若检查出语法斚w问题Q认真调_直到找不到问题所在?/p>
q次PR提交之前Q忘记这回事了,会导致多了若q次ơsubmit提交?/p>
d|:https://github.com/apache/apisix/pulls 新徏一?code>New pull requestQ后面将使用PR指代pull request
?/p>
PR提交标题是规范要求的Q模板如下:
{type}: {desc}
其中{type}
指代本次PRcdQ具体值如下,量不要搞错Q?/p>
feat
Q新功能QfeatureQ?/li>
fix
Q修补bugdocs
Q文档(documentationQ?/li>
style
Q?格式Q不影响代码q行的变动)refactor
Q重构(即不是新增功能,也不是修改bug的代码变动)test
Q增加测?/li>
chore
Q构E或辅助工具的变?/li>
其中{desc}
需要概括本ơ提交内宏V?/p>
比如q次标题为:feat: add consul kv discovery module
?/p>
PR内容模板化,为标准的Github Markdown格式Q主要目的说明本ơ提交内容,C如下Q?/p>
### What this PR does / why we need it:
<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->
### Pre-submission checklist:
* [ ] Did you explain what problem does this PR solve? Or what new features have been added?
* [ ] Have you added corresponding test cases?
* [ ] Have you modified the corresponding document?
* [ ] Is this PR backward compatible? **If it is not backward compatible, please discuss on the [mailing list](https://github.com/apache/apisix/tree/master#community) first**
按照模板格式填写Q省心省力,如下Q?/p>
### What this PR does / why we need it:
As I mentioned previously in the mail-list, my team submit our `consul_kv` discovery module now.
More introductions here:
https://github.com/yongboy/apisix/blob/consul_kv/doc/discovery/consul_kv.md
### Pre-submission checklist:
* [x] Did you explain what problem does this PR solve? Or what new features have been added?
* [x] Have you added corresponding test cases?
* [x] Have you modified the corresponding document?
* [x] Is this PR backward compatible? **If it is not backward compatible, please discuss on the [mailing list](https://github.com/apache/apisix/tree/master#community) first**
提交PR之后Q才是一个开始,L?/p>
Apisix目会自动针Ҏ们所提交内容执行持箋集成Q?code>apisix目的检查项很多Q比如针对Markdown格式很严格Q?/p>
持箋集成不通过Q按照要求微调吧Q也是标准化的要求?/p>
我们在PUSH代码之前Q? make lint
?make license-check
两个命o提前还是十分有必要的,提前语法等?/p>
首先Q一定要保持箋集成不能出错。持l集成通不q,说明我们的准备还不充分,l箋调整修改Ql提交,一直到持箋集成完全执行成功为止?/p>
保证持箋集成执行成功Q这是最基本的要求,否则C无法认我们的代码是否基本合根{?/p>
放松心态,准备开始改qBUGQ以及接受社区的各种代码评审和改q意见吧?/p>
其次Q就是要虚心接受C代码评审和改q意见了Q这是最关键的一步?/p>
下面是一些徏议:
认真对待每一个徏议,有则改之无则加勉Q不知不觉之间就q步了很多,代码质量也得C提升?/p>
l过多次的微调,我们的服务发现核心模块基本上已趋于完善了一版,q已l和q没准备分n出来之前的原始文件相比已l天差地别了 :))
下面是本ơPR包含的多ơ提交、代码评审以及答复等完整程截图Q?br/>
被合q到d支之后,有没有感觉到整个C都在帮助我们一hq,快不快哉 Q?/p>
本次提交的服务发现模块依赖一个组Ӟlua-resty-consul
Q其仓库地址Q?a >https://github.com/hamishforbes/lua-resty-consulQ最新版本ؓQ`0.3.2`。因为我们在实际部v定制Ӟ直接下蝲了该文gQ简单直接粗暴?/a>
?code>apisix目针对目依赖Q采用的 LuaRocks 理Q在 2021-2-20 之前该组件托在 https://luarocks.org/modules/hamish/lua-resty-consul 上面最新版本ؓ 0.2-0
Q这很隑֊了?/p>
我的处理步骤如下Q?/p>
有些一波三?:))
一旦合q到d支后Q后l的演进整个C都可以参与进来,可能有h?issue
Q可能有人提 PR 修改{,后箋我们想ؓ该模块l提交,那将是另外一个PR的事情?/p>
我们可以l箋做以下事情:
毫无疑问Q这是一个良性@环?/p>
参与C开发的其它cd提交Q可能会比上面所q简单很多,但大都可以看做是以上行ؓ的一个子集?/p>
参与开源,也会为我们打开一扇窗P去除自n的狭隘。积极向C靠拢Q这需要磨M些思维或认知的pQ虚心认识到自我的不Iq不断调整不断进步?/p>
加aQ?/p>
U上q行?APISIX ?1.5 版本Q而社区已l发布了 Apisix 2.2Q是时候需要升U到最新版了,能够享受最版本带来的大量的BugFixQ性能增强Q以及新增特性的支持{~
从Apisix 1.5升到Apisix 2.2q程中,不是一帆风的Q中间踩了不坑Q所谓前车之鉴后事之师,q里l大家简单梳理一下我们团队所在具体业务环境下Q升U过E中t的若干坑,以及一些需要避免的若干注意事项{?/p>
下文所说原先版本,皆指Apisix 1.5Q新版则是Apisix 2.2版本?/p>
针对上游Upstream没有使用服务发现的\由来Ԍ本次升没有遇到什么问题?/p>
公司内部U上业务大都ZConsul KV方式实现服务注册和服务发玎ͼ因此我们自行实现了一?consul_kv.lua
模块实现服务发现程?/p>
q在Apisix 1.5下面一切工作正常?/p>
但在Apisix 2.2下面Q就无法直接工作了,原因如下Q?/p>
discovery_type
q行索引原先q行中仅支持一U服务发现机Ӟ需要配|在 apisix
层下面Q?/p>
apisix:
......
discover: consul_kv
......
新版需要直接在config*.yaml
文g中顶层层U下q行配置Q可支持多种不同的\由发现机Ӟ如下Q?/p>
discovery: # service discovery center
eureka:
host: # it's possible to define multiple eureka hosts addresses of the same eureka cluster.
- "http://127.0.0.1:8761"
prefix: "/eureka/"
fetch_interval: 30 # default 30s
weight: 100 # default weight for node
timeout:
connect: 2000 # default 2000ms
send: 2000 # default 2000ms
read: 5000
我们有所变通,直接在配|文仉层配|consul_kv多个集群相关参数Q避?discovery
层q深?/p>
discovery:
consul_kv: 1
consul_kv:
servers:
-
host: "172.19.5.30"
port: 8500
-
host: "172.19.5.31"
port: 8500
prefix: "upstreams"
timeout:
connect: 6000
read: 6000
wait: 60
weight: 1
delay: 5
connect_type: "long" # long connect
......
当然Q这仅仅保证了服务发现模块能够在启动时被正常加蝲?/p>
推荐阅读Q?/p>
Apisix当前同时支持多种服务发现机制Q这个很赞。对应的代hQ就是需要额外引?discovery_type
字段Q用于烦引可能同时存在的多个服务发现机制?/p>
?Cousul KV方式服务发现ZQ那么需要在已有?upstream
对象中需要添加该字段Q?/p>
"discovery_type" : "consul_kv"
原先的一?code>upstream对象Q仅仅需?service_name
字段属性指定服务发现相兛_址卛_Q?/p>
{
"id": "d6c1d325-9003-4217-808d-249aaf52168e",
"name": "grpc_upstream_hello",
......
"service_name": "http://172.19.5.30:8500/v1/kv/upstreams/grpc/grpc_hello",
"create_time": 1610437522,
"desc": "demo grpc service",
"type": "roundrobin"
}
而新版的则需要添?code>discovery_type字段Q表明该service_name
字段对应的具体模块名Uͼ效果如下Q?/p>
{
"id": "d6c1d325-9003-4217-808d-249aaf52168e",
"name": "grpc_upstream_hello",
......
"service_name": "http://172.19.5.30:8500/v1/kv/upstreams/grpc/grpc_hello",
"create_time": 1610437522,
"desc": "demo grpc service",
"type": "roundrobin",
"discovery_type":"consul_kv"
}
后面我们若支持Consul Service或ETCD KV方式服务发现机制Q则会非常弹性和清晰?/p>
调整了配|指令,d上述字段之后Q后端服务发现其实就已经起作用了?/p>
但gRPC代理路由q不会生?#8230;…
在我们的pȝ中,上游和\由是需要单独分开理的,因此创徏的HTTP或GRPC路由需要处理支?code>upstream_id的烦引?/p>
q在1.5版本中,grpc路由是没问题的,但到了apisix 2.2版本中,l护?@spacewander
暂时没做支持Q原因是规划grpc路由和dubbo路由处理逻辑于一_更ؓ紧凑。从l护角度我是认可的,但作Z用者来Ԍq就有些不合理了Q直接丢弃了针对以往数据的支持?/p>
作ؓ当前Geek一些方式,?apisix/init.lua
中,最成?Q优雅和成本成反比)修改如下Q找到如下代码:
-- todo: support upstream id
api_ctx.matched_upstream = (route.dns_value and
route.dns_value.upstream)
or route.value.upstream
直接替换Z面代码即可解决燃眉之急:
local up_id = route.value.upstream_id
if up_id then
local upstreams = core.config.fetch_created_obj("/upstreams")
if upstreams then
local upstream = upstreams:get(tostring(up_id))
if not upstream then
core.log.error("failed to find upstream by id: " .. up_id)
return core.response.exit(502)
end
if upstream.has_domain then
local err
upstream, err = lru_resolved_domain(upstream,
upstream.modifiedIndex,
parse_domain_in_up,
upstream)
if err then
core.log.error("failed to get resolved upstream: ", err)
return core.response.exit(500)
end
end
if upstream.value.pass_host then
api_ctx.pass_host = upstream.value.pass_host
api_ctx.upstream_host = upstream.value.upstream_host
end
core.log.info("parsed upstream: ", core.json.delay_encode(upstream))
api_ctx.matched_upstream = upstream.dns_value or upstream.value
end
else
api_ctx.matched_upstream = (route.dns_value and
route.dns_value.upstream)
or route.value.upstream
end
新版的apisix auth授权插g支持多个授权插g串行执行Q这个功能也很赞Q但此DD了先前ؓ具体业务定制的授权插件无法正常工作,q时需要微调一下?/p>
原先调用方式Q?/p>
local consumers = core.lrucache.plugin(plugin_name, "consumers_key",
consumer_conf.conf_version,
create_consume_cache, consumer_conf)
因ؓ新版?code>lrucache不再提供 plugin
函数Q需要微调一下:
local lrucache = core.lrucache.new({
type = "plugin",
})
......
local consumers = lrucache("consumers_key", consumer_conf.conf_version,
create_consume_cache, consumer_conf)
另一处是Q顺利授权之后,需要赋?code>consumer相关信息Q?/p>
ctx.consumer = consumer
ctx.consumer_id = consumer.consumer_id
此时需要替换成如下方式QؓQ可能存在的Q后l的授权插gl箋作用?/p>
consumer_mod.attach_consumer(ctx, consumer, consumer_conf)
更多请参考:apisix/plugins/key-auth.lua
源码?/p>
q移分ؓ三步Q?/p>
/apisix/upstreams
中包含服务注册的数据Q一一d "discovery_type" : "consul_kv"
属?/li>
Z以上操作之后Q从而完成了ETCD V2到V3的数据迁UR?/p>
我们在运l层面,使用 /usr/local/openresty/bin/openresty -p /usr/local/apisix -g daemon off;
方式q行|关E序?/p>
q也导_自动忽略了官Ҏ倡的Q?code>apisix start 命o自动提前为ETCD V3初始化的一些键值对内容?/p>
因此Q需要提前ؓETCD V3建立以下键值对内容Q?/p>
Key Value
/apisix/routes : init_dir
/apisix/upstreams : init_dir
/apisix/services : init_dir
/apisix/plugins : init_dir
/apisix/consumers : init_dir
/apisix/node_status : init_dir
/apisix/ssl : init_dir
/apisix/global_rules : init_dir
/apisix/stream_routes : init_dir
/apisix/proto : init_dir
/apisix/plugin_metadata : init_dir
不提前徏立的话,׃Dapisix重启后,无法正常加蝲ETCD中已有数据?/p>
其实有一个补救措施,需要修?apisix/init.lua
内容Q找到如下代码:
if not dir_res.nodes then
dir_res.nodes = {}
end
比较geek的行为,使用下面代码替换一下即可完成兼容:
if dir_res.key then
dir_res.nodes = { clone_tab(dir_res) }
else
dir_res.nodes = {}
end
我们Zapisix-dashboard定制开发了大量的针对公司实际业务非常实用的企业U特性,但也D了无法直接升U到最新版的apisix-dashboard?/p>
因ؓ非常基础的上游和路由没有发生多大改变Q因此这部分升的需求可以忽略?/p>
实际上,只是在提交上游表单时Q包含服务注册信息JSON字符串中需要增?discovery_type
字段和对应值即可完成支持?/p>
p了一些时间完成了从Apisix 1.5升到Apisix 2.2的行为,虽然有些坑,但整体来Ԍq算利。目前已l上Uƈ全量部vq行Q目前运行良好?/p>
针对q停留在Apisix 1.5的用P新版增加了Control API以及多种服务发现{新Ҏ支持,q是非常值得升的?/p>
升之前Q不妨仔l阅L一个版本的升日志Q地址Q?a >https://github.com/apache/apisix/blob/2.2/CHANGELOG.md Q,然后需要根据具体业务做好兼Ҏ试准备和准备升步骤Q这些都是非常有必要的?/p>
针对我们团队来讲Q升U到最新版Q一斚w降低了版本升U的压力Q另一斚w也能够辅助我们能参与到开源社Z去,挺好~
最q一D|_要ؓ一个手机终端APPE序从零开始设计一整套HTTP APIQ因为面向的用户很固定,一个新的移动端APP。目前还是项目初期,自然要求一切快速、从Q实用性ؓ丅R?/p>
下面逐一我们是如何设计HTTP APIQ虽然相对大部分言Q没有什么新意,但对我来说很新鲜的。避免忘_着I闲快记录下来?/p>
PHP嘛?团队内也没几个h熟悉?/p>
JavaQ好几年没有过了,那么复杂的解x案,再加上团队内也没什么h?……
团队使用qLuaQ基于OpenResty构徏qTCP、HTTP|关{,对Lua + Nginxl合非常熟悉Q能够快速的应用在线上环境。再说Lua语法y、简单,一个新手半天就可以基本熟悉Q马上开工?/p>
看来QNginx + Lua是目前最为适合我们的了?/p>
HTTP APIQ需要充分利用HTTP具体操作语义Q来应对具体的业务操作方法。基于此Q没有闭门造RQ我们选择?http://lor.sumory.com/ q么一个小巧的框架Q用于辅助HTTP API的开发开发?/p>
嗯,OpenResty + Lua + LorQ就构成了我们简单技术堆栈?/p>
每一具体业务逻辑Q直接在URL Path中体现出来。我们要的是单快速,数据l构之间的连接关p,可能的LE化。egQ?/p>
/resource/video/ID
比如用户反馈q一模块Q将使用下面比较固定的\径:
/user/feedback
GET
Q以用户l度查询反馈的历史列表,可分?
curl -X GET http://localhost/user/feedback?page=1
POST
Q提交一个反?
curl -X POST http://localhost/user/feedback -d "content=hello"
DELETE
Q删除一个或多个反馈Q参数附加在URL路径中?
curl -X DELETE http://localhost/user/feedback?id=1001
PUT
Q更新评论内?
curl -X PUT http://localhost/user/feedback/1234 -d "content=hello2"
用户属性很多,用户늧只是其中一个部分,因此更新늧q一行ؓQHTTP?PATCH
Ҏ可更_և的描q部分数据更新的业务需求:
/user/nickname
PATCH
Q更新用hUͼ늧是用户属性之一Q可以用更轻量U的 PATCH
语义
curl -X PATCH http://localhost/user/nickname -d "nickname=hello2"
嗯,同一cȝ资源URL虽然固定了,但HTTP Method呈现了不同的业务逻辑需求?/p>
实际业务HTTP API的访问是需要授权的?/p>
传统的Access Token解决ҎQ有session回话机制Q一般需要结合Web览器,需要写入到Cookie中,或生产一个JSessionID用于标识{。这针对单纯面向Udl端的HTTP API后端来讲Qƈ没有义务dq一的兼容,略显冗余?/p>
另外是 OAUTH
认证了,有整套的认证Ҏq已工业化,很是成熟了,但对我们而言q是太重Q不太适合轻量U的HTTP APIQ不太可能花费太多的_֊d它的q维工作?/p>
最l选择了轻量?Json Web TokenQ非常紧凑,开即用?/p>
最佛_法是把JWT Token攑֜HTTPh头部中,不至于和其它参数hQ?/p>
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiI2NyIsInV0eXBlIjoxfQ.LjkZYriurTqIpHSMvojNZZ60J0SZHpqN3TNQeEMSPO8" -X GET http://localhost/user/info
下面是一副浏览器D늚一般认证流E,q与HTTP API认证大体一_
JWT的Lua实现Q推? https://github.com/SkyLothar/lua-resty-jwt.git
Q简单够用?/p>
jwt需要和业务q行l定Q结?lor q个API开发框架提供的中间件机Ӟ可在业务处理之前Q在合适位|进行权限拦截?/p>
不同于OAUTHQJWT协议?strong>自包?/strong>Ҏ,军_了后端可以将很多属性信息存攑֜payload负荷中,其token生成之后后端可以不用存储Q下ơ客L发送请求时会发送给服务器端Q后端获取之后,直接验证卛_Q验证通过Q可以直接读取原先保存其中的所有属性?/p>
下面梳理一下Jwt认证和Lor的结合?/p>
app:use(function(req, res, next)
local token = ngx.req.get_headers()["Authorization"]
-- 校验p|Qerr为错误代码,比如 400
local payload, err = verify_jwt(token)
if err then
res:status(err):send("bad access token reqeust")
return
end
-- 注入q当前上下文中,避免每次从token中获? req.params.uid = payload.uid
next()
end)
app:use("/user", function(req, res, next)
if not req.params.uid then
-- 注意Q这里没有调用next()ҎQ请求到q里截止了Q不在匹配后面的路由
res:status(403):send("not allowed reqeust")
else
next() -- 满以上条gQ那么l匹配下一个\? end
end)
local function check_token(req, res, next)
if not req.params.uid then
res:status(403):send("not allowed reqeust")
else
next()
end
end
local function check_master(req, res, next)
if not req.params.uid ~= master_uid then
res:status(403):send("not allowed reqeust")
else
next()
end
end
local lor = require("lor.index")
local app = lor()
-- 声明一个group router
local user_router = lor:Router()
-- 假设查看是不需要用h限的
user_router:get("/feedback", function(req, res, next)
end)
user_router:put("/feedback", check_token, function(req, res, next)
end)
user_router:post("/feedback", check_token, function(req, res, next)
end)
-- 只有理员才有权限删?user_router:delete("/feedback", check_master, function(req, res, next)
end)
-- 以middleware的Ş式将该group router加蝲q来
app:use("/user", user_router())
......
app:run()
我们在上一个项目中对外提供了GraphQL APIQ其Q在试环境下)自n提供文档输出自托机Ӟ再结合方便的调试客户端,实让后端开发和前端APP开发大大降低了频繁交流的频率,节省了若q流量,但前期还是需要较多的培训投入?/p>
但在新项目中Q一度想提供GraphQL APIQ遇到的问题如下Q?/p>
毫无疑问Q以最低成本快速构为完整的APP功能QHTTP API + JSON格式是最服的选择?/p>
虽然有些担心服务器端的输出,很多时候还是会费掉一些流量,客户端ƈ不能够有效的利用q回数据的所有字D属性。但和进度以及h们已l习惯的HTTP API调用方式相比Q又微乎其微了?/p>
当前q一套HTTP API技术堆栈运行的q不错,希望能给有同样需要的同学提供一点点的参考h? :))
当然没有一成不变的架构模型Q随着业务的逐渐发展Q后面相信会有很多的变动。但q是以后的事情了Q谁知道呢,后面有空再次记录吧~
?a href="http://www.tkk7.com/yongboy/archive/2016/07/26/431322.html">TsungW记之压端资源限制?/a>中说到单一IP地址的服务器最多能够向外发?4K个连接,q个已算是极限了?/p>
但现在我q想l箋深入一下,如何H破q个限制?Q?/p>
q部分就是要从多个方面去讨论如何如何H破限制单个IP的限制?/p>
在Tsung 1.6.0 中支持的TCP属性有限,全部Ҏ如下:
protocol_options(#proto_opts{tcp_rcv_size = Rcv, tcp_snd_size = Snd,
tcp_reuseaddr = Reuseaddr}) ->
[binary,
{active, once},
{reuseaddr, Reuseaddr},
{recbuf, Rcv},
{sndbuf, Snd},
{keepalive, true} %% FIXME: should be an option
].
比如可以配置地址重用Q?/p>
<option name="tcp_reuseaddr" value="true" />
q是最为现实、最为方便的办法Q向q维的同事多甌若干个IP地址好。在不考虑其它因素前提下,一个IP地址可以对外建立64K个连接,多个IP是N * 64K
了。这个在Tsung中支持的很好?/p>
<client host="client_99" maxusers="120000" weight="2" cpu="8">
<ip value="10.10.10.99"></ip>
<ip value="10.10.10.11"></ip>
</client>
增加IP可以有多U方式:
ifconfig eth0:2 10.10.10.102 netmask 255.255.255.0
要是没有_的可用虚拟IP地址供你使用Q或怽需要关注一下后面的
IP_TRANSPARENT
Ҏ描q?:))
SO_REUSEPORT
端口重用Ҏ?/h4>
以被压测的一个TCP服务器ؓ例,l箋拿网l四元组说事?/p>
{SrcIp, SrcPort, TargetIp, TargetPort}
SO_REUSEPORT
端口重用Ҏ?- |络四元l中QQ何一个元素值的变化都会成ؓ一个全新的q接
U上有部分服务器安装有CentOS 7Q其内核?.10.0Q很自然支持端口重用Ҏ?/p>
针对只有一个IP地址的压端服务器而言Q端口范围也q定了Q只能从目标服务器连接地址上去考虑。有两种方式Q?/p>
啰嗦了半天,但目前Tsungq没有打要提供支持呢,怎么办,自己动手丰衣食吧:
https://github.com/weibomobile/tsung/commit/f81288539f8e6b6546cb9e239c36f05fc3e1b874
Linux Kernel 2.6.28提供IP_TRANSPARENT
Ҏ,支持可以l定不是本机的IP地址。这UIP地址的绑定不需要显C的配置在物理网卡、虚拟网卡上面,避免了很多手动操作的ȝ。但是需要主动指定这U配|,比如下面的C语言版本代码
int opt =1;
setsockopt(server_socket, SOL_IP, IP_TRANSPARENT, &opt, sizeof(opt));
目前在最新即打包的1.6.1版本中提供了对TCP的支持,也需要翻译成对应的选项Q以便在建立|络q接时用:
K?/p>
说明一下:
- IP_TRANSPARENT
没有对应专门的宏变量Q其具体gؓ19
- SOL_IP
定义宏对应|0
- dSocket选项通用格式为:{raw, Protocol, OptionNum, ValueSpec}
那么如何让透明代理模式工作呢?
IP_TRANSPARENT
Ҏ?/h5>
<options>
...
<option name="ip_transparent" value="true" />
...
<options>
那么q些额外的IP地址如何讄呢?
可以为client元素手动d多个可用的IP地址
<client host="tsung_client1" maxusers="500000" weight="1">
<ip value="10.10.10.117"/>
<ip value="10.10.10.118"/>
......
<ip value="10.10.10.127"/>
</client>
可以使用新增?code>iprangeҎ?/p>
<client host="tsung_client1" maxusers="500000" weight="1">
<ip value="10.10.10.117"/>
<iprange version="v4" value="10.10.10-30.1-254"/>
</client>
但是需要确保:
- q些IP地址目前都没有被已有服务器在使用
- q且可以被正常绑定到物理/虚拟|卡上面
- 完全可用
假设我们?code>tsung_client1q台压测端服务器Q绑定所有额外IP地址到物理网?code>eth1上,那么需要手动添加\p则:
ip rule add iif eth1 tab 100
ip route add local 0.0.0.0/0 dev lo tab 100
q个支持压测端绑定同一|段的可用IP地址Q比如压端IP?72.16.247.130Q?72.16.247.201暂时I闲的话Q那我们可以?72.16.89.201q个IP地址用于压测。此时不要求被压的服务器配|什么?/p>
比如 10.10.10.0 q个D늚IP机房暂时没有使用Q那我们专用于压用,q样一台服务器有?50多个可用的IP地址了?/p>
压测端前面已l配|好了,现在需要ؓ被压的服务器添加\p则,q样在响应数据包的时候能够\由到压测端:
route add -net 10.10.10.0 netmask 255.255.255.0 gw 172.16.247.130
讄完成Q可以通过route -n
命o查看当前所有\p则:
K?/p>
在不需要时Q可以删除掉Q?/p>
route del -net 10.10.10.0 netmask 255.255.255.0
梳理了以上所能够惛_的方式,以尽可能H破单机的限Ӟ核心q是可能找到够多可用的IP地址Q利用Linux内核Ҏ支持,E序层面l定可能多的IP地址Q徏立更多的对外q接。当然以上没有考虑cM于CPU、内存等资源限制Q实际操作时Q还是需要考虑q些资源的限制的?/p>
L说细节、理论,会让Z胜其烦。我们用Tsung来一?00万用户压的吧,或许能够引v好多人的兴趣 :))
下面Q我Ҏ在公司分享的PPT《分布式百万用户压测你的业务》,贴出其中的关键部分,说明q行一?00W(?M)用户压测的执行步骤?/p>
假定面向白用户Q因此才有了下面可执行的10个步骤用于开展分布式百万用户?/p>
K?/p>
看着步骤很多Q一旦熟悉ƈ掌握之后Q中间可以省却若qӀ?/p>
K?/p>
大家在用Tsung之前Q花费一Ҏ间阅d整个用户手册Q虽然是英文的,阅读h也不复杂。读完之后,我们也就知道如何做测试了Q遇到的大部分问题,也能够在里面扑ֈ{案?/p>
K?/p>
K?br/>
K?br/>
K?br/>
K?br/>
K?br/>
K?/p>
K?/p>
因ؓTsung依赖于ErlangQ因此需要首先安装:
wget https://packages.erlang-solutions.com/erlang-solutions-1.0-1.noarch.rpm
rpm -Uvh erlang-solutions-1.0-1.noarch.rpm
sudo yum install erlang
然后再是安装TsungQ徏议直接用Tsung 1.6.0修改版,主要提供IP只连支持Q具体细节,可参考这?http://www.tkk7.com/yongboy/archive/2016/07/28/431354.html Q:
git clone https://github.com/weibomobile/tsung-1.6.0.git
./configure --prefix=/usr/local
make install
tsung—rsh
K?/p>
Z么要替换掉SSHQ主要原因:
可进一步参考:TsungW记之分布式增强跛_SSH绊?/a>?/p>
要把业务定义的所有会话内容完整的整理映射成Tsung的会话内容,因ؓ用户行ؓ很复杂,也需要我们想法设法去模拟?/p>
其实Q演C所使用的是U有协议Q可以参?TsungW记之插件编写篇 ?/p>
当完成压会话内容之后, 我们启动了从节点Q然后从节点被启动,开始执行具体压Q务了?/p>
紧密x服务器服务状态、资源占用等情况对了,最好还要作Z个终端用户参与到产品体验中去?/p>
Tsung压测l束之后Q不会主动生成压结果报表的Q需要借助? 其实Q一旦熟悉ƈ掌握Tsung之后Q步?-6都可以节省了Q@环执行步?-10?/p>
你若以ؓ仅仅只是谈论Tsung如何?M用户压测Q那错了,只要机器资源够,q个目标很Ҏ实现。我们更应该xQ我们压的目的是什么,我们应该x什么,q个应该形成一个完整可循环q程Q驱动着pȝ架构健康先前发展?/p>
6. ~写压测内容
K?br/>
K?br/>
K?/p>
users_100w.xml
文g已经填写完毕Q我们可以开始压了?/p>
7. q行Tsung
K?/p>
8. 压测q程中,我们该做什?/h4>
K?/p>
9. 压测l束Q生成Tsung报表
K?/p>
tsung_stats.pl
perl脚本生成Q要查阅可借助python生成临Web站点Q浏览器打开卛_?/p>
10. 回顾和ȝ
K?/p>
结
Tsung对具体协议、通道的支持,一般以插g形式提供接口Q接口不是很复杂Q插件也很容易编写,支持协议多,也就不为怪了?/p>
下面首先梳理一下当前Tsung 1.6.0所有内|插Ӟ然后Z个名UCؓQmsg的私有二q制协议~写插g, q行Qmsg服务器端E序Q执行压力测试,最后查看测试报告?/p>
Tsung 1.6.0支持的协议很多,单梳理一下:
K?/p>
tsung_config_protocolname
模块解析
ts_protocolname
模块支持数据操作
已经支持协议单说明:
_一Ҏ看Tsung插g的工作流E(点击可以看大图)Q?/p>
攑֤一些(引用 hncscwc 博客囄Q相当赞Q)Q?/p>
Tsung针对通用协议有支持,若是U有或不那么通用的协议,׃会有专门的插件支持了Q那么可选的有两条\子:
既然谈到了插Ӟ我们也编写一个插件也体验一下编写插件的q程?/p>
假设一个虚拟场景,打造一个新的协议QmsgQ二q制格式l成Q?/p>
K?/p>
q种随意假象出来的格式,不妨UC?strong>qmsgQQ可爱形式的messageQ协议,仅作为Demo演示而存在。简单场景:
PocketLen:**##UserId + UserComment##**
PocketLen:**##UserId + RandomCode##**
Z卡哇伊一些,多了一些点~的?*####**”符受?/p>
q里ZTsung 1.6.0版本构徏一个Qmsg插gQ假定你懂一些Erlang代码Q以及熟悉Tsung一些基本概c?/p>
要创建Tsung的一个Qmsg插g目Q虽没有固定规范Q但按照已有格式l织好代码层U也是有必要的?/p>
├── include
│ ?└── ts_qmsg.hrl
├── src
│ ?├── tsung
│ ?│ ?└── ts_qmsg.erl
│ ?└── tsung_controller
│ ? └── ts_config_qmsg.erl
└── tsung-1.0.dtd
Tsung的压以xml文g驱动Q因此需要界定一个Qmsg插g形式的完整会话的XML呈现Q比如:
<session probability="100" name="qmsg-demo" type="ts_qmsg">
<request>
<qmsg uid="1001">Hello Tsung Plugin</qmsg>
</request>
<request>
<qmsg uid="1002">This is a Tsung Plugin</qmsg>
</request>
</session>
ts_qmsg
Q会话类型所依赖协议模拟客户端实?/li>
<qmsg uid="Number">Text</qmsg>
定义了qmsg会话可配|Ş式,内嵌?code>request元素?/li>
uid
为属?/li>
此时Q你若直接在xml文g中编辑,会遇到校验错误?/p>
Tsung的xml文g依赖tsung-1.0.dtd
文gq行校验配置是否有误Q需要做对DTD文g做修改,以支持所d新的协议?/p>
?code>tsung-1.0.dtd目中,最支持:
ts_qmsg
qmsg
:
<!ELEMENT request ( match*, dyn_variable*, ( http | jabber | raw | pgsql | ldap | mysql |fs | shell | job | websocket | amqp | mqtt | qmsg) )>
<!ELEMENT qmsg (#PCDATA) >
<!ATTLIST qmsg
uid CDATA "0"
ack (local | no_ack | parse) #REQUIRED
>
完整内容Q可参?code>tsung_plugin_demo/tsung-1.0.dtd文g?/p>
include/ts_qmsg.hrl
头文?code>include/ts_qmsg.hrl定义数据保存的结构(也称之ؓ记录/recordQ:
-record(qmsg_request, {
uid,
data
}).
-record(qmsg_dyndata, {
none
}
).
ts_config_qmsg.erl
文gQ用于解析和协议Qmsg兌的配|:
- 只需要实?code>parse_config/2唯一Ҏ
- 解析xml文g中所配置Qmsg协议h相关配置
- ?code>ts_config:parse/1在遇到Qmsg协议配置时调?/p>
备注Q?/p>
ts_qmsg.erl
ts_qmsg.erl
模块主要提供Qmsg协议的编解码的完整动? 以及当前协议界定下的用户会话属性设定?/p>
首先需要实现接?code>ts_plugin规范定义的所有需要函敎ͼ定义了参数值和q回倹{?/p>
-behavior(ts_plugin).
...
-export([add_dynparams/4,
get_message/2,
session_defaults/0,
subst/2,
parse/2,
parse_bidi/2,
dump/2,
parse_config/2,
decode_buffer/2,
new_session/0]).
相对来说Q核心ؓ协议的编解码功能Q?/p>
get_message/2
Q构造请求数据,~码成二q制Q上?code>ts_client模块通过Socketq接发送给目标服务?/li>
parse/2
Q?当对响应作出校验?从原始Socket上返回的数据q行解码Q取出协议定义业务内?/li>
q部分代码可以参?tsung_plugin_demo/src/tsung/ts_client.erl
文g?/p>
虽然理论上可以单独编Q生成的beam文g直接拯到已l安装的tsung对应目录下面Q但实际上插件编写过E中要依赖多个tsung的hrl文gQ这造成了依赖\径问题。采用直接和tsung打包一起部|Ԍ实际操作上有些麻烦,
Z节省体力Q用一个shell脚本 - build_plugin.sh
Q方便快速编译、部|Ԍ
# !/bin/bash
cp tsung-1.0.dtd $1/
cp include/ts_qmsg.hrl $1/include/
cp src/tsung_controller/ts_config_qmsg.erl $1/src/tsung_controller/
cp src/tsung/ts_qmsg.erl $1/src/tsung/
cd $1/
make uninstall
./configure --prefix=/usr/local
make install
q里指定安装Tsung的指定目录ؓ
/usr/local
Q可以根据需要修?/p>
需要提前准备好tsung-1.6.0目录Q?/p>
wget http://tsung.erlang-projects.org/dist/tsung-1.6.0.tar.gz
tar xf tsung-1.6.0.tar.gz
在编译Qmsg插g脚本? 指定一下tsung-1.6.0解压后的路径卛_Q?/p>
sh build_plugin.sh /your_path/tsung-1.6.0
后面嘛,q着自动~译和安装呗?/p>
既然有压端Q就需要一个Qmsg协议处理的后端程?code>qmsg_server.erlQ用于接收客LhQ获得用户IDg后,生成一个随机数字,l装成二q制协议Q然后发l客LQ这是全部功能?/p>
q个E序Q简单一个文Ӟ?tsung_plugin_demo
目录下面Q编译运? 默认监听5678端口Q?/p>
erlc qmsg_server.erl && erl -s qmsg_server start
另外Q还提供了一个手动调用接口,方便在Erlang Shell端调试:
%% 下面?qmsg_server:sendmsg(1001, "q里是用户发a").
启动之后Q监听地址 *: 5678
源码见:tsung_plugin_demo/qmsg_server.erl
因ؓ是演C示范,一台LinxuL上就可以q行了:
qmsg-subst-example
会话使用了用户ID个和用户发言内容自动生成机制<tsung loglevel="debug" dumptraffic="false" version="1.0">
<clients>
<client host="localhost" use_controller_vm="true"/>
</clients>
<servers>
<server host="127.0.0.1" port="5678" type="tcp"/>
</servers>
<load>
<arrivalphase phase="1" duration="1" unit="minute">
<users maxnumber="10" interarrival="1" unit="second"/>
</arrivalphase>
</load>
<sessions>
<session probability="10" name="qmsg-example" type="ts_qmsg">
<request>
<qmsg uid="1001" ack="parse">Hello Tsung Plugin Qmsg!</qmsg>
</request>
</session>
<session probability="90" name="qmsg-subst-example" type="ts_qmsg">
<setdynvars sourcetype="random_number" start="3" end="32">
<var name="random_uid"/>
</setdynvars>
<setdynvars sourcetype="random_string" length="13">
<var name="random_txt"/>
</setdynvars>
<request subst="true">
<qmsg uid="%%_random_uid%%" ack="parse">Haha : %%_random_txt%%</qmsg>
</request>
<thinktime value="6"/>
<request subst="true">
<qmsg uid="%%_random_uid%%" ack="parse">This is a Tsung Plugin</qmsg>
</request>
</session>
</sessions>
</tsung>
q部分内容,请参?tsung_plugin_demo/tsung_qmsg.xml
文g?/p>
当Qmsg的压力测试配|文件写好之后,可以开始执行压力测试了Q?/p>
tsung -f tsung_qmsg.xml start
其输出:
tarting Tsung
Log directory is: /root/.tsung/log/20160621-1334
[os_mon] memory supervisor port (memsup): Erlang has closed
[os_mon] cpu supervisor port (cpu_sup): Erlang has closed
其中, 其日志ؓQ?code>/root/.tsung/log/20160621-1334?/p>
q入其生成压日志目录,然后生成报表Q查看压结果哈Q?/p>
cd /root/.tsung/log/20160621-1334
/usr/local/lib/tsung/bin/tsung_stats.pl
echo "open your browser (URL: http://IP:8000/report.html) and vist the report now :))"
/usr/bin/python -m SimpleHTTPServer
嗯,打开你的览器,输出所在服务器的IP地址Q就可以看到压测l果了?/p>
以上代码已经攑օgithub仓库Q?a >https://github.com/weibomobile/tsung_plugin_demo?/p>
实际业务的私有协议内容要比上面Demo出来的Qmsg复杂的多Q但其私有协议插件编写,如上面所q几个步骤,按照规范~写Q单机测试,然后延到分布式集群Q完整流E都是一致的?/p>
嗯,搞定了插Ӟ可以对pȝ愉快地进行压了 :))
压力试和监控分不开Q监控能够记录压过E中状态,方便问题跟踪、定位。本我们将讨论对压客Ltsung client的监控,以及对被压测服务器的资源占用监控{。同Ӟ也涉及到Tsungq行时的实时诊断方式Q这也是对Tsung一些运行时状态的d监控?/p>
压测端(指的是tsung clientQ会攉每一个具体模拟终端用P即ts_client模块Q行为数据,发送给主节点(tsung_controllerQ,供后面统计分析用?/p>
K?/p>
match.log仅仅针对HTTPhQ默认不会写入,除非在HTTP压测指定
<http url="/" method="GET" version="1.1"/>
<match do=’log?when=’match?name=’http_match_200ok?gt;200OK</match>
从节点tsung client所记录日志、需要dump的请?响应数据Q都会交由tsung_controller处理
ts_mon_cacheQ接收到数据l计内存计算Q每500毫秒周期分发l后l模块,起到~冲作用
ts_stats_mon模块接收数据q行内存计算Q结果写入由ts_mon触发
ts_mon负责l计数据最?0U定时写入各统计数据到tsung.log文gQ非实时Q可避免盘IO开销q大问题
tsung/src/tsung_controller/tsung_controller.app.in
对应 {dumpstats_interval, 10000}
tsung.log文g汇集了客Lq接、请求、完整会话、页面以及每一的sum操作l计的完整记录,后箋perl脚本报表分析Z?/p>
ts_mon模块处理tsung.log的最核心模块Q全局唯一q程Q标识ؓ{global, ts_mon}
比如某次单机50万用户压tsung.log日志片段Q?/p>
# stats: dump at 1467620663
stats: users 7215 7215
stats: {freemem,"os_mon@yhg162"} 1 11212.35546875 0.0 11406.32421875 11212.35546875 11346.37109375 2
stats: {load,"tsung_controller@10.10.10.10"} 1 0.0 0.0 0.01171875 0.0 0.01171875 2 17,1 Top
stats: {load,"os_mon@yhg162"} 1 2.3203125 0.0 3.96875 0.9609375 2.7558736313868613 411
stats: {recvpackets,"os_mon@yhg162"} 1 5874.0 0.0 604484 5874 319260.6024390246 410
stats: {sentpackets,"os_mon@yhg162"} 1 8134.0 0.0 593421 8134 293347.0707317074 410
stats: {cpu,"os_mon@yhg162"} 1 7.806645016237821 0.0 76.07377357701476 7.806645016237821 48.0447587419309 411
stats: {recvpackets,"tsung_controller@10.10.10.10"} 1 4164.0 0.0 45938 4164 24914.798543689314 412
stats: {sentpackets,"tsung_controller@10.10.10.10"} 1 4182.0 0.0 39888 4182 22939.191747572815 412
stats: {cpu,"tsung_controller@10.10.10.10"} 1 0.575191730576859 0.0 6.217097016796189 0.575191730576859 2.436491628709831 413
stats: session 137 2435928.551725737 197.4558174045777 2456320.3908691406 2435462.9838867188 2436053.875557659 499863
stats: users_count 0 500000
stats: finish_users_count 137 500000
stats: connect 0 0 0 1004.4912109375 0.278076171875 1.480528250488281 500000
stats: page 139 12.500138756182556 1.1243565417115737 2684.760009765625 0.43115234375 16.094989098940804 30499861
stats: request 139 12.500138756182556 1.1243565417115737 2684.760009765625 0.43115234375 16.094989098940804 30499861
stats: size_rcv 3336 3386044720
stats: size_sent 26132 6544251843
stats: connected -139 0
stats: error_connect_timeout 0 11
tsung.log日志文g可由tsung_stats.pl
脚本提取、分析、整理成报表展示Q其报表的一个摘要截图:
K?/p>
当模拟终端遇到网l连接超时、地址不可辄异常事gӞ最l也会发l主节点的ts_mon模块Q保存到tsung.log文g中?/p>
q种异常记录Q关键词前缀?**error_**
Q?/p>
Errors报表好比客户端出现问题晴雨表Q再加上tsung输出log日志文gQ很清楚的呈现压过E中出现的问题汇集,方便问题快速定位?/p>
K?/p>
当前tsung提供?U方式进行监控目标服务器资源占用情况Q?/p>
大致交互功能Q粗略用一张图表示Q?/p>
K?/p>
看一个最l报表部分呈现吧Q?/p>
K?/p>
tsungҎ务器监控采样手机数据不是很丰富,因ؓ它面向的更ؓ通用的监控需求?/p>
更深层次、更l粒度资源监控,需要自行采集、自行分析了Q一般在商业产品在这斚w会有更明需求?/p>
和前面讲到的l端行ؓ数据采集和服务器端资源监控行为类|tsungq行q程中所产生日志被存储到主节炏V?/p>
tsung使用error_logger记录日志Q主节点tsung_controller启动之后Q会q发启动tsung client从节点,换句话来说tsung client从节Ҏ׃节点tsung_controller创徏Q这个特性决定了tsung client从节点用error_logger记录的日志都会被重定向到主节点tsung_controller所在服务器上,q个是由Erlang自n独特机制军_?/p>
因此Q你在主节点log目录下能够看到具体的日志输出文gQ也水到渠成了。因为Erlang天生分布式基因,从节点error_logger日志输出透明重定向到主节点,不费吹灰之力。这在其他语a看来Q确实完全不可能L实现的?/p>
Zerror_logger包装日志记录Q需要一个步骤:
error_logger:tty(false)
error_logger:logfile({open, LogFile})
?DEBUG/?DEBUGF/?LOG/?LOGF/
debug(From, Message, Args, Level) ->
Debug_level = ?config(debug_level),
if
Level =< Debug_level ->
error_logger:info_msg("~20s:(~p:~p) "++ Message,
[From, Level, self()] ++ Args);
true ->
nodebug
end.
和大部分日志框架讑֮的日志等U一_emergency > critical > error > warning > notice (default) > info > debug
Q从左到叻I依次递减?/p>
需要注意事,error_logger语义录错误日志,只适用于真正的异常情况Qƈ不期望过多的消息量的处理?
若当一般业务调试类型日志量q多Ӟ不但耗费了大量内存,|络/盘写入速度跟不上生产速度Ӟ会导致进E堵塞,严重会拖累整个应用僵死,因此需要在tsung.xml文g中设|至infoU别Q至默认的notice很合适?/p>
Tsung在运行时Q我们可以remote shell方式q接dq去?/p>
Zq接方便Q我写了一个脚?connect_tsung.sh
Q只需要传入tsung节点名称卛_Q?/p>
# !/bin/bash
## 讉Kq程Tsung节点 sh connect\_tsung.sh tsung\_controller@10.10.10.10
HOST=`ifconfig | grep "inet " | grep -v "127.0.0.1" | head -1 | awk '{print $2}' | cut -d / -f 1`
if [ -z $HOST ]; then
HOST = "127.0.0.1"
fi
erl -name tmp\_$RANDOM@$HOST -setcookie tsung -remsh $1
需要安装有Erlangq行时环境支?/p>
当然Q要向运行脚本,你得知道Tsung所有节点名U?/p>
其实有两U方式获得Tsung节点名称Q?/p>
sh connect_tsung.sh tsung_controller@tsung_master_hostname
nodes().
可以获得完整tsung client节点列表其实Q这里仅仅针对用Erlangq且对Tsung感兴的同学Q你都能够进来了Q那么如何进行查看、调试运行时tsungpȝq行情况Q那么就很简单了。推荐?recon 库,包括内存占用Q函数运行堆栈,CPU资源分配{,一目了然?/p>
若问Qtsung启动时如何添加recon依赖Q也不复杂:
tsung_controller主节点启动时Q指定recon依赖库位|?/p>
tsung -X /Your_Save_Path/recon/ebin/ ...
说一个用例,修改监控数据?0U写入tsung.log文g旉间隔|10U修改ؓ5U:
application:set_env(tsung_controller, dumpstats_interval, 5000).
执行之后Q会立刻生效?/p>
ȝ了TsungM监控Q以及服务器端监控部分,以及q行时监控等。提供的被压服务器监控功能很粗Q仅攉CPU、内存、负载、接收数据等cd峰|h一般参考意义。但ZTsung构徏的、或cM商业产品Q一般会有提供专门数据收集服务器Q但对于开源的应用而言Q需要兼N用需求,也是能够理解的?/p>
前面说到设计一个小型的C/Scdq程l端套g以替换SSHQƈ且已l应用到U上。这个问题,其实不是Tsung自n的问题,是外部连接依赖问题?/p>
Tsung在启动分布式压测Ӟ主节?code>tsung_controller要连接的从机必须要填写主机名Q主机名没有内网DNS服务器支持解析的情况?我所l历互联|公司很有提供支持?Q只好费劲在/etc/hosts
文g中填写主机名U和IP地址的映关p,颇ؓȝQ尤其是要添加一Ҏ的压从机或从机变动频率较大时?/p>
那么如何解决q些问题呢,让tsung在复杂的机房内网环境下,完全ZIPq行直连Q这是本文所讨论的内宏V?/p>
完全限定域名Q羃写ؓFQDN (fully qualified domain name)Q?a >赛门铁克l出的中文定?/a>Q?/p>
一U用于指定计机在域层次l构中确切位|的明确域名?br/> 一台特定计机或主机的完整 Internet 域名。FQDN 包括两部分:L名和域名。例?mycomputer.mydomain.com?br/> 一U包含主机名和域名(包括域)?URL。例如,www.symantec.com 是完全限定域名。其?www 是主机,symantec 是二U域Q?com 是顶U域。FQDN L以主机名开始且以顶U域名结束,因此 www.sesa.symantec.com 也是一?FQDN?/p>
若机器主机名为内|域名Ş式,q且支持DNS解析Q方便其它服务器可通过该主机名直接扑ֈ对应IP地址Q能?ping -c 3 机器域名
通,那么机器之间能够Ҏ扑ֈҎ?/p>
服务器hostname的命名,若不是域名Ş式,短名UŞ式,比如“yk_mobile_dianxin_001”,一般内|的DNS服务器不支持解析Q机器之间需要互相在/etc/hosts文g建立彼此IP地址映射关系才能够互相感知对斏V?/p>
因ؓTsung使用Erlang~写QErlang关于节点启动名称规定Q也是Tsung需要面对的问题?/p>
Erlang节点名称一般需要遵循两U格式:
erl -name tsun_node
Tsung处理方式Q?/p>
-F
参数指定使用完全限定域名形式L名称无论是完全限定域名Ş式,q是单的短名UŞ式,当别的主机需要通过L名访问时Q系l层面需要通过DNSpȝ解析成IP地址才能够进行网l连接。当内网DNS能够解析出来IP来,没有什么担心的Q(短名Uͼ解析不出来时Q多半会通过写入到系l的 /etc/hosts
文g中,q样也能够解析成功?/p>
一般机房内|环境,L名称大都是短名称形式Q若需分布式,每一个主Z间都要能够互相联通,最l济做法是直接使用IP地址Q可避免写入大量映射?hosts 文g中,也会避免一些隐患?/p>
默认情况下,Tsung Master主节点名U类g 既然Tsung主节炚w认对IP节点名称支持不够Q改造一?code>tsung/tsung.sh.intsung_controller@L?/code>Q?/p>
脚本?/p>
tsung_controller
Q除非在tsung启动旉过-i
指定前缀Q?/li>
hostname
命o可设|主机名Q?/li>
Tsung启动?code>-F参数为指定?strong>完全限定域名(FQDN)形式Q不支持携带参数。若要直接传逺P地址Q类gQ?/p>
-F Your_IP
修改tsung.sh.in
Q可以传逺P地址Q手动组装节点名Uͼ
F) NAMETYPE="-name"
SERVER_IP=$OPTARG
if [ "$SERVER_IP" != "" ]; then
CONTROLLER_EXTENDS="@$SERVER_IP"
fi
;;
修改不复杂,更多l节请参考:https://github.com/weibomobile/tsung/blob/master/tsung.sh.in
启动TsungӞ指定本地IPQ?/p>
tsung -F 10.10.10.10 -f tsung.xml start
tsung_controller目前节点名称已经变ؓQ?/p>
-name tsung_controller@10.10.10.10
嗯,目标达成?/p>
l出一个节点client50配置Q?/p>
<client host="client50" maxusers="100000" cpu="7" weight="4">
<ip value="10.10.10.50"></ip>
<ip value="10.10.10.51"></ip>
</client>
Tsung Master惌问client50Q需要提前徏立client50与IP地址的映关p:
echo "10.10.10.50 client50" >> /etc/hosts
host
属性默认情况下只能填写长短名称Q无法填写IP地址Qؓ了兼容已有规则,修改tsung-1.0.dtd
文g为client元素新增一?code>hostip属性:
<!ATTLIST client
cpu NMTOKEN "1"
type (machine | batch) "machine"
host NMTOKEN #IMPLIED
hostip CDATA ""
batch (torque | pbs | lsf | oar) #IMPLIED
scan_intf NMTOKEN #IMPLIED
maxusers NMTOKEN "800"
use_controller_vm (true | false) "false"
weight NMTOKEN "1">
修改src/tsung_controller/ts_config.erl
文gQ增加处理逻辑Q只有当主节点主机名为IP时才会取hostip
作ؓL名:
{ok, MasterHostname} = ts_utils:node_to_hostname(node()),
case {ts_utils:is_ip(MasterHostname), ts_utils:is_ip(Host), ts_utils:is_ip(HostIP)} of
%% must be hostname and not ip:
{false, true, _} ->
io:format(standard_error,"ERROR: client config: 'host' attribute must be a hostname, "++ "not an IP ! (was ~p)~n",[Host]),
exit({error, badhostname});
{true, true, _} ->
%% add a new client for each CPU
lists:duplicate(CPU,#client{host = Host,
weight = Weight/CPU,
maxusers = MaxUsers});
{true, _, true} ->
%% add a new client for each CPU
lists:duplicate(CPU,#client{host = HostIP,
weight = Weight/CPU,
maxusers = MaxUsers});
{_, _, _} ->
%% add a new client for each CPU
lists:duplicate(CPU,#client{host = Host,
weight = Weight/CPU,
maxusers = MaxUsers})
end
嗯,现在可以q样配置从节点了Q不用担心Tsung启动时是否附?code>-F参数了:
<client host="client50" hostip="10.10.10.50" maxusers="100000" cpu="7" weight="4">
<ip value="10.10.10.50"></ip>
<ip value="10.10.10.51"></ip>
</client>
其实Q只要你定只用主节点L名ؓIP地址Q可以直接设|host属性gؓIP|可忽略hostip属性,但这以牺牲兼Ҏؓ代h的?/p>
<client host="10.10.10.50" maxusers="100000" cpu="7" weight="4">
<ip value="10.10.10.50"></ip>
<ip value="10.10.10.51"></ip>
</client>
Z减少/etc/hosts
大量映射写入Q还是推荐全部IP形式Q这UŞ式适合Tsung分布式集所依赖服务器的快速租赁模型?/p>
针对Tsung最C码增加的IP直连Ҏ所有修改,已经攑֜github上:
https://github.com/weibomobile/tsung ?/p>
q且已经递交pull request
Q?https://github.com/processone/tsung/pull/189 ?/p>
比较有意思的是,有这样一条评论:
K?/p>
最q一ơ发行版是tsung 1.6.0Q这个版本比较稳定,我实际压所使用的就是在此版本上增加IP直连支持Q如上所qͼQ已l被单独攑օ到github上:
https://github.com/weibomobile/tsung-1.6.0
至于如何安装Q?code>git clone到本圎ͼ后面是如何~译tsung的步骤了Q不再篏q?/p>
若要让IP直连Ҏ生效,再次说明启用步骤一下:
IP直连Q再配合前面所写SSH替换ҎQ可以让Tsung分布式集在复杂|络机房内网环境下适应性向前迈了一大步?/p>
2016-08-06 更新此文Q增加Tsung 1.6.0修改版描q?/p>
Erlang天生支持分布式环境,Tsung框架的分布式压测受益于此Q简单轻松操控子节点生死存亡、派发Q务等不费吹灰之力?/p>
Tsung启动分布式压时Q主节点tsung_controller默认情况下需要通过SSH通道q接到远E机器上启动从节点,那么问题便来了,一般互联网公司Zx/堡垒?|关授权方式讉K机房服务器,那么SSH机制失效Qƈ且被明o止。SSH不通,TsungL启动不了从机Q分布式更无从谈赗?/p>
那么如何解决q个问题呢,让tsung在复杂的机房|络环境讑֮下更加如鱼得_是本文所讨论的内宏V?/p>
RSHQremote shell~写Q维基百U上英文解释Q?a >https://en.wikipedia.org/wiki/Remote_Shell。作Z个终端工PLinux界鸟哥曾l写q?RSH客户端和服务器端搭徏教程?/p>
在CentOS下安装也单:
yum install rsh
Erlang借助于rsh命o行工具通过SSH通道q接C节点启动Tsung应用Q下面可以看到rsh工具本n失去了原本的含义Q类gexec
命o功效?/p>
比如Erlang主节点(假设q个服务器名UCؓnode_master
Qƈ且已l在/etc/hosts文g建立了IP地址映射Q在启动时指定rsh的可选方式ؓSSHQ?/p>
erl -rsh ssh -sname foo -setcookie mycookie
启动之后Q要启动q程L节点名称?code>node_slave的子节点Q?/p>
slave:start(node_slave, bar, "-setcookie mycookie").
上面Erlang启动从节点函敎ͼ最l被译为可执行的shell命oQ?/p>
ssh node_slave erl -detached -noinput -master foo@node_master -sname bar@node_slave -s slave slave_start foo@node_master slave_waiter_0 -setcookie mycookie
erl
命oErlang的启动命令,要求Lnode_slave
自n也要安装了Erlang的运行时环境才行?/p>
从节点的启动命o最l依赖于SSHq接q远E执行,光用一般格式ؓQ?/p>
ssh HOSTNAME/IP Command
q就是基于Erlang构徏的Tsung操控从节点启动的最l实现机制?/p>
其它语言中,Master启动Slave也是如此机制
业界选用SSH机制q接q程Unix/Linux服务器主机,分布式环境下要能够自由免除密码方式启动远E主ZQ这里指的是内部Lan环境Q应用,一般需要设|公钥,需要传递公钥,需要保存到各自机器上,q有l常遇到权限问题Q很是麻烦,q是其一。若要取消某台服务器登陆授权Q则需要被动修改公钥,也是不够灉|?/p>
另外一般互联网公司处于安全考虑都会止公司内部人员直接通过SSH方式d到远E主行操作,q样DSSH通道失效QTsungL通过SSHq接C机ƈ执行命oQ也׃可能了?/p>
其实Q在Z分布式压环境下Q快速租赁、快速借用/归还的模型就很适合。一般公司很会存在专门用于压测的大量空闲机器,但是U上会运行着当前负蝲不高的服务器Q可以拿来用作压客L使用Q用完就归还。因为压不会是长时间运行的服务Q其为短旉行ؓ。这U模式下׃适合复杂的SSH公钥满天飞,后期忘记删除的情况,在压端多的情况下Q无疑也造成q维成本Ȁ增,安全性降低等问题?/p>
现在需要寻找一U新的代替方案,一U适应快速租赁的q程l端实现机制?/p>
没找到很轻量的实玎ͼ可以设计q实现这样一U方案?/p>
轻量U服务端守护q程 = 一个监控端口的q程Q?code>rsh_daemon.shQ?+ 执行命oqo功能(rsh_filter)
rsh_daemon.sh
负责守护q程的管理:
rsh_filter
用于远E传入命令ƈq行处理
rsh_daemon.sh
代码很简单:
#!/bin/bash
# the script using for start/stop remote shell daemon server to replace the ssh server
PORT=19999
FILTER=~/tmp/_tmp_rsh_filter.sh
# the tsung master's hostname or ip
tsung_controller=tsung_controller
SPECIAL_PATH=""
PROG=`basename $0`
prepare() {
cat << EOF > $FILTER
#!/bin/bash
ERL_PREFIX="erl"
while true
do
read CMD
case \$CMD in
ping)
echo "pong"
exit 0
;;
*)
if [[ \$CMD == *"\${ERL_PREFIX}"* ]]; then
exec $SPECIAL_PATH\${CMD}
fi
exit 0
;;
esac
done
EOF
chmod a+x $FILTER
}
start() {
NUM=$(ps -ef|grep ncat | grep ${PORT} | grep -v grep | wc -l)
if [ $NUM -gt 0 ];then
echo "$PROG already running ..."
exit 1
fi
if [ -x "$(command -v ncat)" ]; then
echo "$PROG starting now ..."
ncat -4 -k -l $PORT -e $FILTER --allow $tsung_controller &
else
echo "no exists ncat command, please install it ..."
fi
}
stop() {
NUM=$(ps -ef|grep ncat | grep rsh | grep -v grep | wc -l)
if [ $NUM -eq 0 ]; then
echo "$PROG had already stoped ..."
else
echo "$PROG is stopping now ..."
ps -ef|grep ncat | grep rsh | grep -v grep | awk '{print $2}' | xargs kill
fi
}
status() {
NUM=$(ps -ef|grep ncat | grep rsh | grep -v grep | wc -l)
if [ $NUM -eq 0 ]; then
echo "$PROG had already stoped ..."
else
echo "$PROG is running ..."
fi
}
usage() {
echo "Usage: $PROG <options> start|stop|status|restart"
echo "Options:"
echo " -a <hostname/ip> allow only given hosts to connect to the server (default is tsung_controller)"
echo " -p <port> use the special port for listen (default is 19999)"
echo " -s <the_erl_path> use the special erlang's erts bin path for running erlang (default is blank)"
echo " -h display this help and exit"
exit
}
while getopts "a:p:s:h" Option
do
case $Option in
a) tsung_controller=$OPTARG;;
p) PORT=$OPTARG;;
s) TMP_ERL=$OPTARG
if [ "$OPTARG" != "" ]; then
if [[ "$OPTARG" == *"/" ]]; then
SPECIAL_PATH=$OPTARG
else
SPECIAL_PATH=$OPTARG"/"
fi
fi
;;
h) usage;;
*) usage;;
esac
done
shift $(($OPTIND - 1))
case $1 in
start)
prepare
start
;;
stop)
stop
;;
status)
status
;;
restart)
stop
start
;;
*)
usage
;;
esac
ȝ一下:
ncat
监听19999端口提供bind shell机制Q但限制有限IP可访?/li>
请参考:https://github.com/weibomobile/tsung_rsh/blob/master/rsh_daemon.sh
服务器端已经提供了端口接入ƈ准备好了接收指oQ客LQ?code>rsh_client.shQ可以进行连接和交互了:
rsh_client.sh Host/IP Command
nc
命oQ连接远E主?/li>
一样非常少的代码呈现?/p>
#!/bin/sh
PORT=19999
if [ $# -lt 2 ]; then
echo "Invalid number of parameters"
exit 1
fi
REMOTEHOST="$1"
COMMAND="$2"
if [ "${COMMAND}" != "erl" ]; then
echo "Invalid command ${COMMAND}"
exit 1
fi
shift 2
echo "${COMMAND} $*" | /usr/bin/nc ${REMOTEHOST} ${PORT}
有了SSH替换ҎQ那主节点就可以q样启动了:
erl -rsh ~/.tsung/rsh_client.sh -sname foo -setcookie mycookie
比如当Tsung需要连接到另外一台服务器上启动从节点Ӟ它最l会译成下面命令:
/bin/sh /root/.tsung/rsh_client.sh node_slave erl -detached -noinput -master foo@node_master -sname bar@node_slave -s slave slave_start foo@node_master slave_waiter_0 -setcookie mycookie
客户端脚?code>rsh_client.sh则最l需要执行连接到服务器、ƈ发送命的命令:
echo "erl -detached -noinput -master foo@node_master -sname bar@node_slave -s slave slave_start foo@node_master slave_waiter_0 -setcookie mycookie" | /usr/bin/nc node_slave 19999
q样实C和SSH一L功能了,很简单吧?/p>
为tsung启动d-r
参数指定卛_Q?/p>
tsung -r ~/.tsung/rsh_client.sh -f tsung.xml start
rsh_client.sh
脚本最后一行修改一下,指定目标服务器erlq行命oQ?/p>
#!/bin/sh
PORT=19999
if [ $# -lt 2 ]; then
echo "Invalid number of parameters"
exit 1
fi
REMOTEHOST="$1"
COMMAND="$2"
if [ "${COMMAND}" != "erl" ]; then
echo "Invalid command ${COMMAND}"
exit 1
fi
shift 2
exec echo "/root/.tsung/otp_18/bin/erl $*" | /usr/bin/nc ${REMOTEHOST} 19999
上面脚本所依赖的上下文环境可以是这LQ机房服务器操作pȝ和版本一_我们把Erlang 18.1整个q行时环境在一台机器上已经安装的目录(比如目录名ؓotp_18Q,拯到远E主?code>/root/.tsung/目录Q相比于安装而言Q可以让Tsungq行依赖的Eralng环境完全可以UL化(PortableQ,一ơ安装,多次复制?/p>
本文所谈及代码Q都已经托管在githubQ?br/> https://github.com/weibomobile/tsung_rsh
后箋代码更新、BUG修复{,L接参考该仓库?/p>
单一套新的替换SSH通道无密钥登陆远E主机C/S模型Q虽然完整性上无法与SSH相比Q但胜在单够用,完全满了当前业务需要,q且其运l成本低Q无疑让Tsung在复杂服务器内网环境下适应性又朝前多走了半里\?/p>
下一将介绍为Tsung增加IP直连Ҏ支持,使其分布式网l环境下适应性更q泛一些?/p>
q里汇集一下媄响tsung client创徏用户数的各项因素。因为Tsung是IO密集型的应用QCPU占用一般不大,Z可能的生成更多的用P需要考虑内存相关事宜?/p>
Linuxpȝ端口为shortcd表示Q数g限ؓ65535。假讑ֈ配压业务可用端口范围ؓ1024 - 65535Q不考虑可能q运行着其它对外q接的服务,真正可用端口也就?4000左右Q实际上Q一般ؓ了方便计,一般直接设定ؓ50000Q。换a之,卛_一台机器上一个IPQ可用同时对外徏?4000|络q接?/p>
若是N个可用IPQ理Z 64000*NQ实际上q需要满I
另外q需要考虑端口的快速回收等Q可以这样做Q?/p>
sysctl -w net.ipv4.tcp_syncookies=1
sysctl -w net.ipv4.tcp_tw_reuse=1
sysctl -w net.ipv4.tcp_tw_recycle=1
sysctl -w net.ipv4.tcp_fin_timeout=30
sysctl -w net.ipv4.ip_local_port_range="1024 65535"
sysctl -p
若已l在 /etc/sysctl.conf 文g中有记录Q则需要手动修?/p>
作ؓ附加Q可讄端口重用Q?/p>
<option name="tcp_reuseaddr" value="true"/>
注意Q不要设|下面的可用端口范围Q?/p>
<option name="ports_range" min="1025" max="65535"/>
因ؓ操作pȝ会自动蟩q已l被占用本地端口Q而Tsung只能够被动通过错误q行可用端口+1l箋下一个连接,有些多余?/p>
每一个client支持多个可用IP地址列表
<client host="client_99" maxusers="120000" weight="2" cpu="8">
<ip value="10.10.10.99"></ip>
<ip value="10.10.10.11"></ip>
</client>
tsung client从节点开始准备徏立网l连接会话时Q需要从tsung_controller主节点获取具体的会话信息Q其中就包含了客Lq接需要用到来源{LocalIPQ?LocalPort}二元l。由tsung_controller主节点完成?/p>
get_user_param(Client,Config)->
{ok, IP} = choose_client_ip(Client),
{ok, Server} = choose_server(Config#config.servers, Config#config.total_server_weights),
CPort = choose_port(IP, Config#config.ports_range),
{{IP, CPort}, Server}.
choose_client_ip(#client{ip = IPList, host=Host}) ->
choose_rr(IPList, Host, {0,0,0,0}).
......
choose_client_ip(#client{ip = IPList, host=Host}) ->
choose_rr(IPList, Host, {0,0,0,0}).
choose_rr(List, Key, _) ->
I = case get({rr,Key}) of
undefined -> 1 ; % first use of this key, init index to 1
Val when is_integer(Val) ->
(Val rem length(List))+1 % round robin
end,
put({rr, Key},I),
{ok, lists:nth(I, List)}.
%% 默认不设|?ports_range 会直接返?
%% 不徏议设|?<option name="ports_range" min="1025" max="65535"/>
%% 因ؓq样存在端口冲突问题Q除非确实不存被占用情况
choose_port(_,_, undefined) ->
{[],0};
choose_port(Client,undefined, Range) ->
choose_port(Client,dict:new(), Range);
choose_port(ClientIp,Ports, {Min, Max}) ->
case dict:find(ClientIp,Ports) of
{ok, Val} when Val =< Max ->
NewPorts=dict:update_counter(ClientIp,1,Ports),
{NewPorts,Val};
_ -> % Max Reached or new entry
NewPorts=dict:store(ClientIp,Min+1,Ports),
{NewPorts,Min}
end.
从节点徏立到压测服务器连接时Q就需要指定从主节点获取到的本机IP地址和端口两元组Q?/p>
Opts = protocol_options(Protocol, Proto_opts) ++ [{ip, IP},{port,CPort}],
......
gen_tcp:connect(Server, Port, Opts, ConnectTimeout).
若从机单个网卡绑定了多个IPQ又懒于输入Q可以配|扫描特?
<ip scan="true" value="eth0"/>
本质上用shell方式获取IP地址Qƈ且支持CentOS 6/7?/p>
/sbin/ip -o -f inet addr show dev eth0
因ؓ扫描比较慢,Tsung 1.6.1推出?code>ip_rangeҎ支持?/p>
pȝ打开文g句柄Q直接决定了可以同时打开的网l连接数量,q个需要设|大一些,否则Q你可能会在tsung_controller@IP.log文g中看?code>error_connect_emfilecM文g句柄不够使用的警告,此D大于 > N * 64000?/p>
echo "* soft nofile 300000" >> /etc/security/limits.conf
echo "* hard nofile 300000" >> /etc/security/limits.conf
或者,在Tsung会话启动脚本文g中明添加上ulimit -n 300000
?/p>
一个网lSocketq接占用不多Q但上万个或数十万等׃容小觑了Q设|不当会D内存直接成ؓ屏障?/p>
Tsung默认讄的网lSocket发送接收缓冲区?6KBQ一般够用了?/p>
以TCPZQ某ơ我手误为Tcp接收~存赋D?599967字节)Q这h一个网l了解至占用了0.6M内存Q直接导致在16G内存服务上网l连接数?万多Ӟ内存告急?/p>
<option name="tcp_snd_buffer" value="16384"></option>
<option name="tcp_rcv_buffer" value="16384"></option>
此g覆盖Linuxpȝ讄接收、发送缓冲大?/p>
_略的默认D,一个网l连接发送缓冲区 + 接收~冲区,再加上进E处理连接堆栈占用,U?0多K内存Qؓ卌方便,讑֮建立一个网l连接消?0K内存?/p>
先不考虑其它因素Q若我们惌从机模拟10W个用P那么当前可用内存臛_要剩余:50K * 100000 / 1000K = 5000M = 5G内存。针对一般服务器来讲Q完全可满要求Q剩下事情就是要有两个可用IP了)?/p>
使用ErlangE序写的应用服务器,q程要存储堆栈调用信息,q程一多久会占用大量内存,惌服务更多|络q接/dQ需要将不活动的q程讄Z眠状态,以便节省内存QTsung的压会话信息若包含thinktime旉Q也要考虑启用hibernate休眠机制?/p>
<option name="hibernate" value="5"></option>
值单位秒Q默认thinktime过10U后自动启动Q这里修改ؓ5U?/p>
tsung使用error_logger记录日志Q其只适用于真正的异常情况Q若当一般业务调试类型日志量q多Ӟ不但耗费了大量内存,|络/盘写入速度跟不上生产速度Ӟ会导致进E堵塞,严重会拖累整个应用僵死,因此需要在tsung.xml文g中设|日志等U要高一些,臛_默认的notice很合适?/p>
dump是一个耗时的行为,因此默认为falseQ除非很的压测用户用于调试?/p>
<option name="file_server" id="userdb" value="/your_path/100w_users.csv"/>
...
<setdynvars sourcetype="file" fileid="userdb" delimiter=";" order="iter">
<var name="userid" />
<var name="nickname" />
</setdynvars>
...
<request subst="true">
<yourprotocol type="hello" uid="%%_userid%%" ack="local">
Hello, I'm %%_nickname%%
</yourprotocol>
</request>
讑֮一个有状态的场景Q用户ID储存在文件中Q每一ơ会话请求都要从获取到用户IDQ压用户一旦达到百万别ƈ且用hU生速率q大Q比如每U?000个用PQ会l常遇到时错误Q?/p>
=ERROR REPORT==== 25-Jul-2016::15:14:11 ===
** Reason for termination =
** {timeout,{gen_server,call,
[{global,ts_file_server},{get_next_line,userdb}]}}
q是因ؓQ当tsung client遇到setdynvars
指oӞ会直接请求主机ts_file_server模块Q当一旉h量巨大,可能会造成单一模块处理~慢Q出现超旉题?/p>
怎么办:
某些时候,要避免tsung client压测端媄响所在服务器|络带宽IO太拥挤,需要限制流量,光用o牌桶法?/p>
<option name="rate_limit" value="1024"></option>
阀D方式:
{RateConf,SizeThresh} = case RateLimit of
Token=#token_bucket{} ->
Thresh=lists:min([?size_mon_thresh,Token#token_bucket.burst]),
{Token#token_bucket{last_packet_date=StartTime}, Thresh};
undefined ->
{undefined, ?size_mon_thresh}
end,
接收传入量数据Q需要计:
handle_info2({gen_ts_transport, _Socket, Data}, wait_ack, State=#state_rcv{rate_limit=TokenParam}) when is_binary(Data)->
?DebugF("data received: size=~p ~n",[size(Data)]),
NewTokenParam = case TokenParam of
undefined ->
undefined;
#token_bucket{rate=R,burst=Burst,current_size=S0, last_packet_date=T0} ->
{S1,_Wait}=token_bucket(R,Burst,S0,T0,size(Data),?NOW,true),
TokenParam#token_bucket{current_size=S1, last_packet_date=?NOW}
end,
{NewState, Opts} = handle_data_msg(Data, State),
NewSocket = (NewState#state_rcv.protocol):set_opts(NewState#state_rcv.socket,
[{active, once} | Opts]),
case NewState#state_rcv.ack_done of
true ->
handle_next_action(NewState#state_rcv{socket=NewSocket,rate_limit=NewTokenParam,
ack_done=false});
false ->
TimeOut = case (NewState#state_rcv.request)#ts_request.ack of
global ->
(NewState#state_rcv.proto_opts)#proto_opts.global_ack_timeout;
_ ->
(NewState#state_rcv.proto_opts)#proto_opts.idle_timeout
end,
{next_state, wait_ack, NewState#state_rcv{socket=NewSocket,rate_limit=NewTokenParam}, TimeOut}
end;
下面则是具体的o牌桶法Q?/p>
%% @spec token_bucket(R::integer(),Burst::integer(),S0::integer(),T0::tuple(),P1::integer(),
%% Now::tuple(),Sleep::boolean()) -> {S1::integer(),Wait::integer()}
%% @doc Implement a token bucket to rate limit the traffic: If the
%% bucket is full, we wait (if asked) until we can fill the
%% bucket with the incoming data
%% R = limit rate in Bytes/millisec, Burst = max burst size in Bytes
%% T0 arrival date of last packet,
%% P1 size in bytes of the packet just received
%% S1: new size of the bucket
%% Wait: Time to wait
%% @end
token_bucket(R,Burst,S0,T0,P1,Now,Sleep) ->
S1 = lists:min([S0+R*round(ts_utils:elapsed(T0, Now)),Burst]),
case P1 < S1 of
true -> % no need to wait
{S1-P1,0};
false -> % the bucket is full, must wait
Wait=(P1-S1) div R,
case Sleep of
true ->
timer:sleep(Wait),
{0,Wait};
false->
{0,Wait}
end
end.
以上单梳理一下媄响tsung从机创徏用户的各因素,实际环境其实相当复杂Q需要一一对症下药才行?/p>
接着上文Qtsung一旦启动,M节点之间需要协调分配资源,完成分布式压Q务?/p>
Erlang SDK提供了从机启动方式:
slave:start(Host, Node, Opts)
启动从机需要借助于免登陆形式q程l端Q比如SSHQ后l会讨论SSH存在不Q以及全新的替代品)Q需要自行配|?/p>
<client host="client_100" maxusers="60000" weight="1">
<ip value="10.10.10.100"/>
</client>
tsung10@client_100
Opts
表示相关参数单翻译一下:slave:start(client_100, 'tsung10@client_100', Opts)
从机需要关闭时Q就很简单了Q?/p>
slave:stop(Node)
当然若主Z途挂掉,从机也会自动自杀掉自w?/p>
TsungL启动从机成功Q从机和L可以Erlang节点q程之间q行Ҏ调用和消息传递。潜在要求是Qtsung~译后beam文g能够在Erlangq行时环境中能够讉K刎ͼq个和Java Classpath一致原理?/p>
rpc:multicall(RemoteNodes,tsung,start,[],?RPC_TIMEOUT)
到此为止Q一个tsung client实例成功q行?/p>
明白了主从启动方式,下面讨论压测目标Q比?0万用L量,Ҏl出的压从机列表,q行d分配?/p>
tsung压测xml配置文gQload元素可以配置Md生成的信息?/p>
<load>
<arrivalphase phase="1" duration="60" unit="minute">
<!--users maxnumber="500000" interarrival="0.004" unit="second"></users-->
<users maxnumber="500000" arrivalrate="250" unit="second"></users>
</arrivalphase>
</load>
所说从节点也是压测客户端,需要配|clients元素Q?/p>
<clients>
<client host="client_100" maxusers="60000" weight="1">
<ip value="10.10.10.100"/>
</client>
......
<client host="client_109" maxusers="120000" weight="2">
<ip value="10.10.10.109"></ip>
<ip value="10.10.10.119"></ip>
</client>
</clients>
在《Tsung Documentation》给ZQ一个CPU一个tsung client实例Q?/p>
Note: Even if an Erlang VM is now able to handle several CPUs (erlang SMP), benchmarks shows that it’s more efficient to use one VM per CPU (with SMP disabled) for tsung clients. Only the controller node is using SMP erlang.
Therefore, cpu should be equal to the number of cores of your nodes. If you prefer to use erlang SMP, add the -s option when starting tsung (and don’t set cpu in the config file).
%% add a new client for each CPU
lists:duplicate(CPU,#client{host = Host,
weight = Weight/CPU,
maxusers = MaxUsers})
若要讄单个tsung client实例׃n多个CPUQ此时不要设|cpu属性啦Q,需要在tsung启动时添?code>-s参数Qtsung client被启动时Qsmp属性被讄成autoQ?/p>
-smp auto +A 8
q样从机只有一个tsung client实例了,不会让h产生困扰。若是时租借从机,启动时?s参数Qƈ且要去除cpu属性设|,q样才能够自动共享所有CPU核心?/p>
假设client元素配置maxusers
数量?KQ那么实际上被分配数量ؓ10K(压测人数多,压测从机?Ӟ那么tsung_controller
会l分裂新的tsung client实例Q直?0K用户数量完成?/p>
<client host="client_98" maxusers="1000" weight="1">
<ip value="10.10.10.98"></ip>
</client>
tsung client分配的数量超q自w可服务上限用户Ӟq里讄的是1KQ时Q关闭自w?/p>
launcher(_Event, State=#launcher{nusers = 0, phases = [] }) ->
?LOG("no more clients to start, stop ~n",?INFO),
{stop, normal, State};
launcher(timeout, State=#launcher{nusers = Users,
phase_nusers = PhaseUsers,
phases = Phases,
phase_id = Id,
started_users = Started,
intensity = Intensity}) ->
BeforeLaunch = ?NOW,
case do_launch({Intensity,State#launcher.myhostname,Id}) of
{ok, Wait} ->
case check_max_raised(State) of
true ->
%% let the other beam starts and warns ts_mon
timer:sleep(?DIE_DELAY),
{stop, normal, State};
false->
......
end;
error ->
% retry with the next user, wait randomly a few msec
RndWait = random:uniform(?NEXT_AFTER_FAILED_TIMEOUT),
{next_state,launcher,State#launcher{nusers = Users-1} , RndWait}
end.
tsung_controller接收从节炚w出通知Q但分配L没有完成Q会启动新的tsung client实例Q一样先启动从节点,然后再启动tsung client实例Q。整个过E串行方式@环,直到10K用户数量完成Q?/p>
%% start a launcher on a new beam with slave module
handle_cast({newbeam, Host, Arrivals}, State=#state{last_beam_id = NodeId, config=Config, logdir = LogDir}) ->
Args = set_remote_args(LogDir,Config#config.ports_range),
Seed = Config#config.seed,
Node = remote_launcher(Host, NodeId, Args),
case rpc:call(Node,tsung,start,[],?RPC_TIMEOUT) of
{badrpc, Reason} ->
?LOGF("Fail to start tsung on beam ~p, reason: ~p",[Node,Reason], ?ERR),
slave:stop(Node),
{noreply, State};
_ ->
ts_launcher_static:stop(Node), % no need for static launcher in this case (already have one)
ts_launcher:launch({Node, Arrivals, Seed}),
{noreply, State#state{last_beam_id = NodeId+1}}
end;
一个tsung client分配的用hQ可以理解ؓ会话d数。Tsung以终端可以模拟的用户为维度进行定义压?/p>
所有配|tsung client元素Q设|M1Q权重相加之和ؓL重TotalWeightQ用hL为MaxMemberQ一个tsung client实例QL设ؓM2Q分配的模拟用户数可能ؓQ?/p>
MaxMember*(Weight/TotalWeight)
需要注意:
- M2 >= M1
- 若压阶D?code><arrivalphase元素配置duration
D,于最l用?0万用h照每U?50速率耗时旉Q最l分配用h小于期望?/p>
<clients>
<client host="localhost" use_controller_vm="true"/>
</clients>
没有物理从机Q主从节炚w在一台机器上Q需要设|?code>use_controller_vm="true"。相比tsung集群Q单一节点tsung启动很单,M之间不需要SSH通信Q直接内部调用?/p>
local_launcher([Host],LogDir,Config) ->
?LOGF("Start a launcher on the controller beam ~p~n", [Host], ?NOTICE),
LogDirEnc = encode_filename(LogDir),
%% set the application spec (read the app file and update some env. var.)
{ok, {_,_,AppSpec}} = load_app(tsung),
{value, {env, OldEnv}} = lists:keysearch(env, 1, AppSpec),
NewEnv = [ {debug_level,?config(debug_level)}, {log_file,LogDirEnc}],
RepKeyFun = fun(Tuple, List) -> lists:keyreplace(element(1, Tuple), 1, List, Tuple) end,
Env = lists:foldl(RepKeyFun, OldEnv, NewEnv),
NewAppSpec = lists:keyreplace(env, 1, AppSpec, {env, Env}),
ok = application:load({application, tsung, NewAppSpec}),
case application:start(tsung) of
ok ->
?LOG("Application started, activate launcher, ~n", ?INFO),
application:set_env(tsung, debug_level, Config#config.loglevel),
case Config#config.ports_range of
{Min, Max} ->
application:set_env(tsung, cport_min, Min),
application:set_env(tsung, cport_max, Max);
undefined ->
""
end,
ts_launcher_static:launch({node(), Host, []}),
ts_launcher:launch({node(), Host, [], Config#config.seed}),
1 ;
{error, Reason} ->
?LOGF("Can't start launcher application (reason: ~p) ! Aborting!~n",[Reason],?EMERG),
{error, Reason}
end.
每一个tsung clientq行着一?code>ts_launch/ts_launch_static本地注册模块Q掌控终端模拟用L成和会话控制?/p>
maxusers
上限
L按照xml配置生成全局用户产生速率Q从机按照自w权重分配的速率q行单独控制Q这也是d分解的具体呈现?/p>
在Tsung中用L成速度UC为强度,Ҏ所配置的load属性进行配|?/p>
<load>
<arrivalphase phase="1" duration="60" unit="minute">
<users maxnumber="500000" arrivalrate="250" unit="second"></users>
</arrivalphase>
</load>
关键属性:
interarrival
Q生成压用L旉间隔arrivalrate
Q单位时间内生成用户数量parse(Element = #xmlElement{name=users, attributes=Attrs},
Conf = #config{arrivalphases=[CurA | AList]}) ->
Max = getAttr(integer,Attrs, maxnumber, infinity),
?LOGF("Maximum number of users ~p~n",[Max],?INFO),
Unit = getAttr(string,Attrs, unit, "second"),
Intensity = case {getAttr(float_or_integer,Attrs, interarrival),
getAttr(float_or_integer,Attrs, arrivalrate) } of
{[],[]} ->
exit({invalid_xml,"arrival or interarrival must be specified"});
{[], Rate} when Rate > 0 ->
Rate / to_milliseconds(Unit,1);
{InterArrival,[]} when InterArrival > 0 ->
1/to_milliseconds(Unit,InterArrival);
{_Value, _Value2} ->
exit({invalid_xml,"arrivalrate and interarrival can't be defined simultaneously"})
end,
lists:foldl(fun parse/2,
Conf#config{arrivalphases = [CurA#arrivalphase{maxnumber = Max,
intensity=Intensity}
|AList]},
Element#xmlElement.content);
tsung_controller
Ҏ一个tsung client生成用户强度分解?ClientIntensity = PhaseIntensity * Weight / TotalWeight
Q?code>1000 * ClientIntensity是易读的每U生成用户速率倹{?/p>
get_client_cfg(Arrival=#arrivalphase{duration = Duration,
intensity= PhaseIntensity,
curnumber= CurNumber,
maxnumber= MaxNumber },
{TotalWeight,Client,IsLast} ) ->
Weight = Client#client.weight,
ClientIntensity = PhaseIntensity * Weight / TotalWeight,
NUsers = round(case MaxNumber of
infinity -> %% only use the duration to set the number of users
Duration * ClientIntensity;
_ ->
TmpMax = case {IsLast,CurNumber == MaxNumber} of
{true,_} ->
MaxNumber-CurNumber;
{false,true} ->
0;
{false,false} ->
lists:max([1,trunc(MaxNumber * Weight / TotalWeight)])
end,
lists:min([TmpMax, Duration*ClientIntensity])
end),
?LOGF("New arrival phase ~p for client ~p (last ? ~p): will start ~p users~n",
[Arrival#arrivalphase.phase,Client#client.host, IsLast,NUsers],?NOTICE),
{Arrival#arrivalphase{curnumber=CurNumber+NUsers}, {ClientIntensity, NUsers, Duration}}.
前面讲到每一个tsung client被分配用h公式为:min(Duration * ClientIntensity, MaxNumber * Weight / TotalWeight)
Q?/p>
再看一下launch加蝲一个终端用hQ会自动Ҏ当前分配用户生成压力pL获得ts_stats:exponential(Intensity)
下一个模拟用户生等待生成的最长时_单位为毫U?/p>
do_launch({Intensity, MyHostName, PhaseId})->
%%Get one client
%%set the profile of the client
case catch ts_config_server:get_next_session({MyHostName, PhaseId} ) of
{'EXIT', {timeout, _ }} ->
?LOG("get_next_session failed (timeout), skip this session !~n", ?ERR),
ts_mon:add({ count, error_next_session }),
error;
{ok, Session} ->
ts_client_sup:start_child(Session),
X = ts_stats:exponential(Intensity),
?DebugF("client launched, wait ~p ms before launching next client~n",[X]),
{ok, X};
Error ->
?LOGF("get_next_session failed for unexpected reason [~p], abort !~n", [Error],?ERR),
ts_mon:add({ count, error_next_session }),
exit(shutdown)
end.
ts_stats:exponential逻辑引入了指数计:
exponential(Param) ->
-math:log(random:uniform())/Param.
l箋往下看吧,隐藏了部分无关代码:
launcher(timeout, State=#launcher{nusers = Users,
phase_nusers = PhaseUsers,
phases = Phases,
phase_id = Id,
started_users = Started,
intensity = Intensity}) ->
BeforeLaunch = ?NOW,
case do_launch({Intensity,State#launcher.myhostname,Id}) of
{ok, Wait} ->
...
{continue} ->
Now=?NOW,
LaunchDuration = ts_utils:elapsed(BeforeLaunch, Now),
%% to keep the rate of new users as expected,
%% remove the time to launch a client to the next
%% wait.
NewWait = case Wait > LaunchDuration of
true -> trunc(Wait - LaunchDuration);
false -> 0
end,
?DebugF("Real Wait = ~p (was ~p)~n", [NewWait,Wait]),
{next_state,launcher,State#launcher{nusers = Users-1, started_users=Started+1} , NewWait}
...
error ->
% retry with the next user, wait randomly a few msec
RndWait = random:uniform(?NEXT_AFTER_FAILED_TIMEOUT),
{next_state,launcher,State#launcher{nusers = Users-1} , RndWait}
end.
下一个用L成需要等?code>Wait - LaunchDuration毫秒旉?/p>
l出一个采h据,只有一个从机,q且用户产生速度1U一个,׃?0个用P
<load>
<arrivalphase phase="1" duration="50" unit="minute">
<users maxnumber="10" interarrival="1" unit="second"/>
</arrivalphase>
</load>
采集日志部分Q记录了Wait
旉|其实M旉q需要加?code>LaunchDurationQ虽然这个值很)Q?/p>
ts_launcher:(7:<0.63.0>) client launched, wait 678.5670934164623 ms before launching next client
ts_launcher:(7:<0.63.0>) client launched, wait 810.2982455546687 ms before launching next client
ts_launcher:(7:<0.63.0>) client launched, wait 1469.2208436232288 ms before launching next client
ts_launcher:(7:<0.63.0>) client launched, wait 986.7202548184069 ms before launching next client
ts_launcher:(7:<0.63.0>) client launched, wait 180.7484423006169 ms before launching next client
ts_launcher:(7:<0.63.0>) client launched, wait 1018.9190235965457 ms before launching next client
ts_launcher:(7:<0.63.0>) client launched, wait 1685.0156394273606 ms before launching next client
ts_launcher:(7:<0.63.0>) client launched, wait 408.53992361334065 ms before launching next client
ts_launcher:(7:<0.63.0>) client launched, wait 204.40900996137086 ms before launching next client
ts_launcher:(7:<0.63.0>) client launched, wait 804.6040921461512 ms before launching next client
M来说Q每一个用L成间隔间不是固定|是一个大U|有偏差,但接q于目标讑֮Q?000毫秒生成一个用h准间隔)?/p>
关于会话的说明:
模拟l端用户模块?code>ts_clientQ状态机Q,挂蝲?code>ts_client_sup下,?code>ts_launcher/ts_launcher_static调用ts_client_sup:start_child(Session)
启动Q是压测d的最l执行者,承包了所有脏累差的活Q?/p>
K?/p>
单梳理主从之间启动方式,从机数量分配{略Q以具体压测d如何在从Z分配和运行等内容?/p>
本篇讲解Tsung大致功能l成、结构,以及M模型Q以便M上掌握?/p>
K?/p>
tsung_controller
?tsung
q两个模块,负责分布式压的核心功能?/p>
从代码层ơ梳理一下tsung目功能l成l构Q便于一目了Ӟ方便直接索引?/p>
K?/p>
讑֮环境为分布式环境下Tsung集群Q下面简单梳理一下主、从节点启动程?/p>
K?/p>
程大致说明Q?/p>
q种模型下:
下面一张图单说明了M之间核心模块交互程Q虽然粗略,核心点也是涉及C?/p>
K?/p>
后面会对具体协议部分有更l论q?/p>
其实是承接上一个流E图Q已l启动了一个ts_client模块Q即执行一个完整生命周期会话模拟终端。它的开启依赖于Tsung Controller启动ts_launch/ts_launch_static模块?/p>
大致程囑֦下:
K?/p>
ZErlang天生分布式基因支持,从节点的生死存亡完全受Tsung主节点的控制Q按需创徏QQ务完成结束,M协调行云水般顺畅?/p>
嗯,后面介l主从实现的一些细节?/p>
有测试驱动的开发模式,目的在于保业务层面功能是准的Q每一ơ新增、修改等动作保都不会媄响到现有功能。功能开发完成了Q需要部|到U上Q系l能够承载多大的用户量呢Q这时候就需要借助于性能压测Q也UC为压力测试,界定pȝ能够承蝲具体定w上限Q从容应对业务的q营需要,扩容或羃容,心中有底?/p>
工欲善其事,必先利其器。掌握一U压工Pq切实应用到实践环境中,q以此不断P代,压力试驱动推动所开发后端应用处理性能逐渐完善?/p>
目前成熟的支持支持TCP、HTTP{连接通道的压工具不,以前接触qApache JMeterQ后面又接触q?a href="tsung.erlang-projects.org">TsungQ因为在实际环境下用比较多Q支持丰富的业务场景定义Qƈ且可扩展性强Q因此Tsung强力推荐之?/p>
MQTsung是一Ƒּ源的高性能分布式压力测试工P支持可编E的情景化测试方案,要向发挥它的Ҏ,依赖于h们的惌力和创造性?/p>
软g/pȝ架构往往着gMl构Q这个可以是一个逐渐完善的过E。这U自我的不断完善的驱动往往来自于实c线上考验。而压力测试可以提供一U推动,心力暴露着架构在性能定w存在的一些不_~陷Q促使着向着更好的方向发展?/p>
pȝ的构Z赖于具体参与执行的hQ就是一资q工程师,业务上每一ơ功能的快速更q、Q何潜在局部修攚w会导致媄响、拖垮整体性能Q这是Z常说??a >蝴蝶效应“,牵一发而动全n?/p>
如何提早感知q且提早修复Q这需要压力测试的驱动Qƈ且压力测试应该成Z个常规化的例行行为,日常化的动作。在每一ơ修改之后,都要q一轮的压测的碾压之后,提供当前后端应用处理的性能、容量等具体指标Q用于指导后l业务上U业务的开展?/p>
在一般互联网公司Q一般线上程序修改后之后Q需要经qQA团队/部门全部功能回归、校验之后才能够上线Q往往~少压测环节Q因Z/她们q不保证pȝ处理性能和容量是否恶化,pȝ的性能建立在系lM的功能上Q如何避免在性能上出现”牵一发而动全n“,有条件的QA同学/团队考虑增加性能压测环节Q功?+ 性能双重回归Q修改媄响点清晰、透明化?/p>
本系列笔讎ͼZtsung-1.6.0源码基础上分析,q行环境为Linux Centos 6?/p>
W记列表Q?/p>
Z方便理解Q一些用词说明:
参与一个实时性交互强的项目,从一开始单机支撑不?万用戗^均请求响应时间约900毫秒Q到目前混合部v的单机支?0万用戗^均响应时间ؓ16毫秒Q这个过E中Tsung不断的压推动着架构逐渐E_、系l承载容量、QPS优化{完全达标。这是一个压力测试驱动性能改进的流E,每一步的改进能够得到正向反馈?/p>
q一pdW记Q所谈核心是TsungQ无论是认知q是改进Q最l都是ؓ了理解利器的Ҏ面面Q方便着手于实践环境中,压测所带来的能量能够驱动我们的E序/服务性能提升、稳定运行,q而更好方便我们进行容量规划、线上部|等?/p>