# 在 Kubernetes 上使用 Jmeter 运行压力测试

Kubernetes 的资源和任务调度能力，能给自动化测试提供相当大力的支持，这里以 Jmeter 为例，讲讲如何在 Kubernetes 中使用 Jmeter 进行简单的性能测试。

## 开始之前

- 录制任务：本文所用镜像为 Jmeter 3.x 版本，建议提前录制一个简单的测试任务进行下面的操作。

- 支持 Jobs 的 Kubernetes 集群，以及缺省的 StorageClass 支持，能够实现 PVC 的动态供应。

- 互联网连接。

## 试验内容

1. 搭建一个 Web DAV 服务，用于提供给 Jmeter 输入输出场所，也便于日后 CI/CD 工具的案例输入或结果输出。
2. 运行单实例的 Jmeter 测试任务。
3. 运行集群形式的 Jmeter 测试任务。

## 预备存储

> 这一步骤并非强制，完全可以通过 scp 或者 mount 等其他方式来实现

这里我们做一个 Web DAV 服务，挂载一个 PVC，在其中分为 input 和 output 两个目录，实际使用过程中，可以进一步按照任务或者 Job 对目录进行更详尽的规划。

首先创建名为`jmeter-task`的存储卷：

~~~yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: jmeter-task
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Gi
~~~

存储卷创建之后，可以使用 `cadaver` 或者 `WinSCP` 等工具去建立目录。

接下来上传 `*.jmx` 文件，到 `input` 目录之中，这里我录制的一个持续访问京东首页的任务，命名为 `jd.jmx`。

## 单实例测试

单实例测试很容易，使用 Kubernetes 的 Job 方式即可：

~~~yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: jmeter
spec:
  template:
    metadata:
      name: jmeter
    spec:
      restartPolicy: Never
      containers:
      - name: jmeter
        image: dustise/jmeter-server
        command:
          - "/jmeter/bin/jmeter"
          - "-n"
          - "-t"
          - "/jmeter/input/jd.jmx"
          - "-l"
          - "/jmeter/output/log"
          - "-j"
          - "/jmeter/output/joker"
        volumeMounts:
        - name: data
          mountPath: /jmeter/input
          subPath: input
        - name: data
          mountPath: /jmeter/output
          subPath: output
      volumes:
      - name: data
        persistentVolumeClaim:
          claimName: jmeter-task
~~~

上面的定义中：

- 任务 Pod 加载了存储卷 `jmeter-task`。使用 `subPath` 指令，分别挂载了输入和输出目录。

- 使用 `-n -t` 的方式运行测试任务，并把输出文件定位到 `output` 目录中。

接下来就可以使用 `kubectl create -f jobs1.yaml` 来运行这一任务。

任务启动之后，可以：

- 使用 `kubectl get jobs` 来查看任务运行状况。

- `kubectl get pods --show-all` 查看任务 Pod。

- `kubectl logs -f [pod name` 查看任务输出。

最后任务会变成完成状态，就可以在 Web DAV 中查看任务报告了。

## 集群测试

Jmeter 可以使用控制台+负载机的形式，使用多个节点进行压力测试，这里需要解决的一个最重要问题就是，在指派任务给负载机时，Jmeter 需要使用 `-R host:port` 的参数，来指定任务要调用的负载机。这一通信是无法通过 Kubernetes 方式的 Service 来完成的。必须建立 Pod 之间的通信，而 Pod 的主机名地址是很飘逸的，同时，我们还是希望负载节点的数量能够实现较为自由的伸缩，因此解决方法就只有 StatefulSet 了。

这个 YAML 很长，所以放在最后了，说说其中的要点：

- 注解中的 `security.alpha.kubernetes.io/sysctls`：实际运行中，jmeter 负载机是需要对内核参数进行一点调整的，Pod 中可以用这一方式进行调整，`https://kubernetes.io/docs/concepts/cluster-administration/sysctl-cluster/` 中有更详细的关于这方面的内容讲解。

- `spec.affinity`：这里设置 Jmeter Pod 尽量分布在不同节点上。

- `RMI_HOST`环境变量：使用每个 Pod 的 IP 为这一变量赋值。

- Service：利用这个 Headless 服务，为每个 Pod 提供主机名支持。

启动这个 Statefulset 之后，会看到规律创建的 Pod 名称：

    jnode-0                   1/1       Running   0          1h
    jnode-1                   1/1       Running   0          1h

对应的主机名称就应该是 jnode-0.jfarm，jnode-1.jfarm。所以上面的 `job.yaml` 可以新增 `-R jnode-0.jfarm:1099,jnode-1.jfarm:1099` 即可。

使用 `kubectl create` 启动任务之后，查看该任务 Pod 的日志，会出现大致这样的内容：

    Creating summariser <summary>
    Created the tree successfully using /jmeter/input/jd.jmx
    Configuring remote engine: jnode-0.jfarm:1099
    Configuring remote engine: jnode-1.jfarm:1099
    Starting remote engines
    Starting the test @ Thu Nov 16 07:24:14 GMT 2017 (1510817054558)
    Remote engines have been started
    Waiting for possible Shutdown/StopTestNow/Heapdump message on port 4445
    summary +    302 in 00:01:16 =    4.0/s Avg:  2967 Min:  2627 Max:  5457 Err:     0 (0.00%) Active: 0 Started: 20 Finished: 20
    summary +     98 in 00:00:00 = 3062.5/s Avg:  3270 Min:  2635 Max:  7192 Err:     0 (0.00%) Active: 0 Started: 20 Finished: 20
    summary =    400 in 00:01:16 =    5.3/s Avg:  3041 Min:  2627 Max:  7192 Err:     0 (0.00%)
    Tidying up remote @ Thu Nov 16 07:25:31 GMT 2017 (1510817131966)
    ... end of run

可以看到，成功配置远程负载服务器之后，测试开始，最后成功完成。

## Statefulset 源码

~~~yaml
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: jnode
  labels:
    app: jmeter
    component: node
spec:
  serviceName: jfarm
  replicas: 2
  selector:
    matchLabels:
      app: jmeter
      component: node
  template:
    metadata:
      labels:
        app: jmeter
        component: node
      annotations:
        security.alpha.kubernetes.io/sysctls: net.ipv4.ip_local_port_range=10000 65000,net.ipv4.tcp_syncookies=1
    spec:
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              topologyKey: kubernetes.io/hostname
              labelSelector:
                matchExpressions:
                - key: app
                  operator: In
                  values:
                  - jmeter
                - key: component
                  operator: In
                  values:
                  - node
      restartPolicy: Always
      containers:
      - name: jmeter
        image: dustise/jmeter-server
        ports:
        - name: server
          containerPort: 1099
        - name: rmi
          containerPort: 20000
        env:
        - name: RMI_HOST
          valueFrom:
            fieldRef:
              fieldPath: status.podIP
---
apiVersion: v1
kind: Service
metadata:
  name: jfarm
  labels:
    app: jmeter
spec:
  clusterIP: None
  ports:
  - port: 1099
    name: server
  selector:
    app: jmeter
    component: node
~~~
