├── __init__.py
├── hztest
├── __init__.py
├── rpc
│ ├── __init__.py
│ ├── protocol.py
│ ├── script
│ │ └── internal-test.py
│ ├── events.py
│ ├── master.py
│ └── slave.py
├── uitest.py
├── wsgi.py
├── urls.py
├── settings.py
├── templates
│ └── hello.html
└── views.py
├── db.sqlite3
├── docker
├── requirements.txt
├── dockerfile_locust
├── dockerfile_slave
└── dockerfile_master
├── manage.py
└── README.md
/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/hztest/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/db.sqlite3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/biyunfei/locustM/HEAD/db.sqlite3
--------------------------------------------------------------------------------
/docker/requirements.txt:
--------------------------------------------------------------------------------
1 | locustio
2 | Django==1.11.10
3 | psutil==5.4.3
--------------------------------------------------------------------------------
/hztest/rpc/__init__.py:
--------------------------------------------------------------------------------
1 | from . import master
2 | from .protocol import Message
3 |
--------------------------------------------------------------------------------
/hztest/uitest.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | import Tkinter
3 |
4 | top = Tkinter.Tk()
5 | hello = Tkinter.Label(top, text='Hello world!')
6 | hello.pack()
7 | quit = Tkinter.Button(top, text='QUIT', command=top.quit, bg='red', fg='white')
8 | quit.pack(fill=Tkinter.X, expand=1)
9 | Tkinter.mainloop()
10 |
--------------------------------------------------------------------------------
/docker/dockerfile_locust:
--------------------------------------------------------------------------------
1 | # 使用Python官方镜像作为镜像的基础
2 | FROM python:2.7-slim
3 | MAINTAINER biyunfei
4 | # 添加vim和gcc依赖
5 | RUN apt-get update && apt-get install -y vim gcc \
6 | # 用完包管理器后安排打扫卫生可以显著的减少镜像大小
7 | && apt-get clean \
8 | && apt-get autoclean \
9 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
10 | # 设置工作空间为/app
11 | WORKDIR /app
12 | # 安装requirements.txt中指定的依赖
13 | ADD requirements.txt /app
14 | RUN pip install -r requirements.txt
--------------------------------------------------------------------------------
/docker/dockerfile_slave:
--------------------------------------------------------------------------------
1 | # 使用Python官方镜像作为镜像的基础
2 | FROM locust
3 | MAINTAINER biyunfei
4 | # 把当前目录下的文件拷贝到 容器里的/app里
5 | ADD ../hztest /app/hztest
6 | # 设置工作空间为/app
7 | WORKDIR /app/hztest/hztest/rpc
8 | # 设置时区
9 | RUN echo "Asia/Shanghai" > /etc/timezone
10 | RUN dpkg-reconfigure -f noninteractive tzdata
11 | # 开放8000端口
12 | EXPOSE 6666 6667 5557 5558 8089 8000
13 | # 设置 HOST 这个环境变量
14 | ENV HOST 0
15 | # 当容器启动时,运行app.py
16 | ENTRYPOINT python slave.py ${HOST}
--------------------------------------------------------------------------------
/docker/dockerfile_master:
--------------------------------------------------------------------------------
1 | # 使用Python官方镜像作为镜像的基础
2 | FROM locust
3 | MAINTAINER biyunfei
4 | # 把当前目录下的文件拷贝到 容器里的/app里
5 | ADD ../hztest /app/hztest
6 | # 设置工作空间为/app
7 | WORKDIR /app/hztest
8 | # 设置时区
9 | RUN echo "Asia/Shanghai" > /etc/timezone
10 | RUN dpkg-reconfigure -f noninteractive tzdata
11 | # 开放8000端口
12 | EXPOSE 6666 6667 5557 5558 8089 8000
13 | # 设置 HOST 这个环境变量
14 | ENV HOST 0
15 | # 当容器启动时,运行app.py
16 | ENTRYPOINT python manage.py runserver ${HOST}:8000
--------------------------------------------------------------------------------
/hztest/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for hztest project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "hztest.settings")
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/hztest/rpc/protocol.py:
--------------------------------------------------------------------------------
1 | import msgpack
2 |
3 |
4 | class Message(object):
5 | def __init__(self, message_type, data, node_id):
6 | self.type = message_type
7 | self.data = data
8 | self.node_id = node_id
9 |
10 | def serialize(self):
11 | return msgpack.dumps((self.type, self.data, self.node_id))
12 |
13 | @classmethod
14 | def unserialize(cls, data):
15 | msg = cls(*msgpack.loads(data, encoding='utf-8'))
16 | return msg
17 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "hztest.settings")
7 | try:
8 | from django.core.management import execute_from_command_line
9 | except ImportError:
10 | # The above import may fail for some other reason. Ensure that the
11 | # issue is really that Django is missing to avoid masking other
12 | # exceptions on Python 2.
13 | try:
14 | import django
15 | except ImportError:
16 | raise ImportError(
17 | "Couldn't import Django. Are you sure it's installed and "
18 | "available on your PYTHONPATH environment variable? Did you "
19 | "forget to activate a virtual environment?"
20 | )
21 | raise
22 | execute_from_command_line(sys.argv)
23 |
--------------------------------------------------------------------------------
/hztest/urls.py:
--------------------------------------------------------------------------------
1 | """hztest URL Configuration
2 |
3 | The `urlpatterns` list routes URLs to views. For more information please see:
4 | https://docs.djangoproject.com/en/1.11/topics/http/urls/
5 | Examples:
6 | Function views
7 | 1. Add an import: from my_app import views
8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
9 | Class-based views
10 | 1. Add an import: from other_app.views import Home
11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
12 | Including another URLconf
13 | 1. Import the include() function: from django.conf.urls import url, include
14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
15 | """
16 | from django.conf.urls import url
17 | from django.contrib import admin
18 | from . import views
19 |
20 | urlpatterns = [
21 | url(r'^$', views.hello),
22 | # url(r'^admin/', admin.site.urls),
23 | ]
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # locustM
2 | It is a web tool for schedule locust test of distributed locust clients;
3 |
4 | To run it on you computer, you should be install locustio, Django==1.11.10, psutil==5.4.3 by PIP as first, now it support in python 2.x; or you can use the docker images which in the next;
5 |
6 | There are two parts of this tools: Master and Slave;
7 |
8 | Master:
9 | In CMD line, at . folder run:
10 |
11 | python manage.py runserver 0:8000
12 |
13 | Open http://localhost:8000 to get in the web tool for control you locust clients; http://localhost:8089 to locust web spawn mode;
14 |
15 | Detail information can see in the source code;
16 |
17 | Slave:
18 | For each locust client, in CMD line: at ./hztest/rpc folder run:
19 |
20 | python slave.py MASTER_IP
21 |
22 | # Make docker images:
23 | docker build -t locust -f dockerfile_locust .
24 | docker build -t locust:master -f dockerfile_master .
25 | docker build -t locust:slave -f dockerfile_slave .
26 |
27 | # Run docker images:
28 | **Master:**
29 | docker run -p 8000:8000 -p 6666:6666 -p 6667:6667 -p 5557:5557 -p 5558:5558 -p 8089:8089 -it --rm locust:master
30 |
31 | **Slave:**
32 | docker run -e HOST={MASTER_IP} -it --rm locust:slave
33 |
--------------------------------------------------------------------------------
/hztest/rpc/script/internal-test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 |
3 | from locust import HttpLocust, TaskSet, task, events
4 | import logging
5 | import datetime
6 | import os
7 | from gevent._semaphore import Semaphore
8 | all_locusts_spawned = Semaphore()
9 | all_locusts_spawned.acquire()
10 |
11 |
12 | def on_hatch_complete(**kwargs):
13 | all_locusts_spawned.release()
14 |
15 | events.hatch_complete += on_hatch_complete
16 |
17 |
18 | class YJKTask(TaskSet):
19 | _headers = {"Content-Type": "application/json; charset=UTF-8"}
20 |
21 | # 并发用户初始化
22 | def on_start(self):
23 | """ on_start is called when a Locust start before any task is scheduled """
24 | # self.login()
25 | all_locusts_spawned.wait()
26 |
27 | @task(1)
28 | def get(self):
29 | with self.client.get('/', catch_response=True) as resp:
30 | if resp.status_code != 200:
31 | resp.failure('Request failure!HTTP Response Status Code: %s, HTTP Response content: %s!' % (resp.status_code, resp.content))
32 | self.locust.logger.error('index.json Request failure!HTTP Response Status Code: %s, HTTP Response content: %s!' % (resp.status_code, resp.content))
33 |
34 |
35 | class YJKUser(HttpLocust):
36 | # 设置Locust压力测试主机地址,用户任务类
37 | host = "https://www.baidu.com"
38 | task_set = YJKTask
39 |
40 | # 设置日志文件及格式
41 | if not os.path.exists('./log'):
42 | os.mkdir('./log')
43 | logging.basicConfig(level=logging.INFO)
44 | fh = logging.FileHandler('./log/log_%s.txt' % datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S'), mode='w')
45 | formatter = logging.Formatter('[%(asctime)s] - %(name)s - %(levelname)s - %(message)s')
46 | fh.setFormatter(formatter)
47 | logger = logging.getLogger() #__name__)
48 | logger.addHandler(fh)
49 |
50 |
51 | # 用户测试场景的等待时间ms,随机时间在min和max之间
52 | min_wait = 0
53 | max_wait = 0
54 | # 压测时间s,到时自动停止
55 | # stop_timeout = 600
56 |
--------------------------------------------------------------------------------
/hztest/settings.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | """
3 | Django settings for hztest project.
4 |
5 | Generated by 'django-admin startproject' using Django 1.11.10.
6 |
7 | For more information on this file, see
8 | https://docs.djangoproject.com/en/1.11/topics/settings/
9 |
10 | For the full list of settings and their values, see
11 | https://docs.djangoproject.com/en/1.11/ref/settings/
12 | """
13 |
14 | import os
15 |
16 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
17 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
18 |
19 |
20 | # Quick-start development settings - unsuitable for production
21 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
22 |
23 | # SECURITY WARNING: keep the secret key used in production secret!
24 | SECRET_KEY = 'koi9newyzk8*igr*0j+)$dd&nkc&yn)jkx8_!-br^qwzb3g3t8'
25 |
26 | # SECURITY WARNING: don't run with debug turned on in production!
27 | DEBUG = True
28 |
29 | ALLOWED_HOSTS = ['*']
30 |
31 |
32 | # Application definition
33 |
34 | INSTALLED_APPS = [
35 | 'django.contrib.admin',
36 | 'django.contrib.auth',
37 | 'django.contrib.contenttypes',
38 | 'django.contrib.sessions',
39 | 'django.contrib.messages',
40 | 'django.contrib.staticfiles',
41 | ]
42 |
43 | MIDDLEWARE = [
44 | 'django.middleware.security.SecurityMiddleware',
45 | 'django.contrib.sessions.middleware.SessionMiddleware',
46 | 'django.middleware.common.CommonMiddleware',
47 | # 'django.middleware.csrf.CsrfViewMiddleware',
48 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
49 | 'django.contrib.messages.middleware.MessageMiddleware',
50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
51 | ]
52 |
53 | ROOT_URLCONF = 'hztest.urls'
54 |
55 | TEMPLATES = [
56 | {
57 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
58 | 'DIRS': [BASE_DIR + "/hztest/templates", ],
59 | 'APP_DIRS': True,
60 | 'OPTIONS': {
61 | 'context_processors': [
62 | 'django.template.context_processors.debug',
63 | 'django.template.context_processors.request',
64 | 'django.contrib.auth.context_processors.auth',
65 | 'django.contrib.messages.context_processors.messages',
66 | ],
67 | },
68 | },
69 | ]
70 |
71 | WSGI_APPLICATION = 'hztest.wsgi.application'
72 |
73 |
74 | # Database
75 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases
76 |
77 | DATABASES = {
78 | 'default': {
79 | 'ENGINE': 'django.db.backends.sqlite3',
80 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
81 | }
82 | }
83 |
84 |
85 | # Password validation
86 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
87 |
88 | AUTH_PASSWORD_VALIDATORS = [
89 | {
90 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
91 | },
92 | {
93 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
94 | },
95 | {
96 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
97 | },
98 | {
99 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
100 | },
101 | ]
102 |
103 |
104 | # Internationalization
105 | # https://docs.djangoproject.com/en/1.11/topics/i18n/
106 |
107 | LANGUAGE_CODE = 'en-us'
108 |
109 | TIME_ZONE = 'Asia/Shanghai'
110 |
111 | USE_I18N = True
112 |
113 | USE_L10N = True
114 |
115 | USE_TZ = True
116 |
117 |
118 | # Static files (CSS, JavaScript, Images)
119 | # https://docs.djangoproject.com/en/1.11/howto/static-files/
120 |
121 | STATIC_URL = '/static/'
122 |
--------------------------------------------------------------------------------
/hztest/templates/hello.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Locust压测调度
6 |
7 |
8 | {{hello}}
9 | 操作说明:
10 | 1.每个压测客户端单独设置运行步骤:
11 | a)发送压测脚本(可多选,单个可以编辑后再发送至客户端)
12 | b)选择压测脚本(多选则一个进程运行一个脚本,单选则所有进程用同一脚本)
13 | c)设置压测进程数(0代表CPU逻辑核心总数),点击ready启动客户机压测进程;
14 | 2.启动Locust Master服务;
15 | 3.打开locust控制WEB:http://{{host}}:8089
16 | 4.测试完成点击各客户端running按钮停止,更换压测脚本,重新启动客户端压测进程;
17 |
18 |
85 |
86 |
--------------------------------------------------------------------------------
/hztest/rpc/events.py:
--------------------------------------------------------------------------------
1 | class EventHook(object):
2 | """
3 | Simple event class used to provide hooks for different types of events in Locust.
4 |
5 | Here's how to use the EventHook class::
6 |
7 | my_event = EventHook()
8 | def on_my_event(a, b, **kw):
9 | print "Event was fired with arguments: %s, %s" % (a, b)
10 | my_event += on_my_event
11 | my_event.fire(a="foo", b="bar")
12 | """
13 |
14 | def __init__(self):
15 | self._handlers = []
16 |
17 | def __iadd__(self, handler):
18 | self._handlers.append(handler)
19 | return self
20 |
21 | def __isub__(self, handler):
22 | self._handlers.remove(handler)
23 | return self
24 |
25 | def fire(self, **kwargs):
26 | for handler in self._handlers:
27 | handler(**kwargs)
28 |
29 | request_success = EventHook()
30 | """
31 | *request_success* is fired when a request is completed successfully.
32 |
33 | Listeners should take the following arguments:
34 |
35 | * *request_type*: Request type method used
36 | * *name*: Path to the URL that was called (or override name if it was used in the call to the client)
37 | * *response_time*: Response time in milliseconds
38 | * *response_length*: Content-length of the response
39 | """
40 |
41 | request_failure = EventHook()
42 | """
43 | *request_failure* is fired when a request fails
44 |
45 | Event is fired with the following arguments:
46 |
47 | * *request_type*: Request type method used
48 | * *name*: Path to the URL that was called (or override name if it was used in the call to the client)
49 | * *response_time*: Time in milliseconds until exception was thrown
50 | * *exception*: Exception instance that was thrown
51 | """
52 |
53 | locust_error = EventHook()
54 | """
55 | *locust_error* is fired when an exception occurs inside the execution of a Locust class.
56 |
57 | Event is fired with the following arguments:
58 |
59 | * *locust_instance*: Locust class instance where the exception occurred
60 | * *exception*: Exception that was thrown
61 | * *tb*: Traceback object (from sys.exc_info()[2])
62 | """
63 |
64 | report_to_master = EventHook()
65 | """
66 | *report_to_master* is used when Locust is running in --slave mode. It can be used to attach
67 | data to the dicts that are regularly sent to the master. It's fired regularly when a report
68 | is to be sent to the master server.
69 |
70 | Note that the keys "stats" and "errors" are used by Locust and shouldn't be overridden.
71 |
72 | Event is fired with the following arguments:
73 |
74 | * *client_id*: The client id of the running locust process.
75 | * *data*: Data dict that can be modified in order to attach data that should be sent to the master.
76 | """
77 |
78 | slave_report = EventHook()
79 | """
80 | *slave_report* is used when Locust is running in --master mode and is fired when the master
81 | server receives a report from a Locust slave server.
82 |
83 | This event can be used to aggregate data from the locust slave servers.
84 |
85 | Event is fired with following arguments:
86 |
87 | * *client_id*: Client id of the reporting locust slave
88 | * *data*: Data dict with the data from the slave node
89 | """
90 |
91 | hatch_complete = EventHook()
92 | """
93 | *hatch_complete* is fired when all locust users has been spawned.
94 |
95 | Event is fire with the following arguments:
96 |
97 | * *user_count*: Number of users that was hatched
98 | """
99 |
100 | quitting = EventHook()
101 | """
102 | *quitting* is fired when the locust process in exiting
103 | """
104 |
105 | master_start_hatching = EventHook()
106 | """
107 | *master_start_hatching* is fired when we initiate the hatching process on the master.
108 |
109 | This event is especially usefull to detect when the 'start' button is clicked on the web ui.
110 | """
111 |
112 | master_stop_hatching = EventHook()
113 | """
114 | *master_stop_hatching* is fired when terminate the hatching process on the master.
115 |
116 | This event is especially usefull to detect when the 'stop' button is clicked on the web ui.
117 | """
118 |
119 | locust_start_hatching = EventHook()
120 | """
121 | *locust_start_hatching* is fired when we initiate the hatching process on any locust worker.
122 | """
123 |
124 | locust_stop_hatching = EventHook()
125 | """
126 | *locust_stop_hatching* is fired when terminate the hatching process on any locust worker.
127 | """
128 |
--------------------------------------------------------------------------------
/hztest/rpc/master.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | r"""
3 | master负责与slave端进行通信,发送来自web的请求给slave,接收slave的结果;
4 | 主要有1个全局master监听服务,2个全局队列用于消息和结果传递,两个gevent协程,分别为master_cmd:接收cmd_queue进行派发给slave,master_listener:接收slave的消息反馈并返回给web端;
5 | """
6 | import zmq.green as zmq
7 | import multiprocessing
8 | from protocol import Message
9 | import six
10 | import gevent
11 | from gevent.pool import Group
12 | import socket
13 | import events
14 | import sys, time
15 | import logging
16 | import signal
17 | from multiprocessing import Queue, Lock
18 |
19 | # 定义全局变量:指令队列,结果队列,进程锁,日志
20 | cmd_queue = Queue()
21 | result_queue = Queue()
22 | lock = Lock()
23 | STATE_INIT, STATE_RUNNING, STATE_STOPPED = ["ready", "running", "stopped"]
24 | sh = logging.StreamHandler()
25 | sh.setFormatter(logging.Formatter('[%(asctime)s] - %(message)s'))
26 | logger = logging.getLogger()
27 | logger.addHandler(sh)
28 | logger.setLevel(logging.INFO)
29 |
30 |
31 | class BaseSocket(object):
32 |
33 | def send(self, msg):
34 | self.sender.send(msg.serialize())
35 |
36 | def recv(self):
37 | data = self.receiver.recv()
38 | return Message.unserialize(data)
39 |
40 | def noop(self, *args, **kwargs):
41 | """ Used to link() greenlets to in order to be compatible with gevent 1.0 """
42 | pass
43 |
44 |
45 | class Server(BaseSocket):
46 | def __init__(self, host, port):
47 | context = zmq.Context()
48 | self.receiver = context.socket(zmq.PULL)
49 | try:
50 | self.receiver.bind("tcp://%s:%i" % (host, port))
51 | except:
52 | sys.exit(0)
53 | self.sender = context.socket(zmq.PUSH)
54 | try:
55 | self.sender.bind("tcp://%s:%i" % (host, port+1))
56 | except:
57 | sys.exit(0)
58 |
59 |
60 | class Master(Server):
61 | def __init__(self, *args, **kwargs):
62 | super(Master, self).__init__(*args)
63 |
64 | class SlaveNodesDict(dict):
65 | def get_by_state(self, state):
66 | return [c for c in six.itervalues(self) if c.state == state]
67 |
68 | @property
69 | def ready(self):
70 | return self.get_by_state(STATE_INIT)
71 |
72 | @property
73 | def running(self):
74 | return self.get_by_state(STATE_RUNNING)
75 |
76 | self.cmd_queue = kwargs['cmd_queue']
77 | self.result_queue = kwargs['result_queue']
78 | self.clients = SlaveNodesDict()
79 | self.greenlet = Group()
80 | # 加载gevent协程,一个用于接收web端发来的指令消息,另一个用于接收slave反馈的消息
81 | self.greenlet.spawn(self.master_listener).link_exception(callback=self.noop)
82 | self.greenlet.spawn(self.master_cmd).link_exception(callback=self.noop)
83 |
84 | def on_quitting():
85 | self.quit()
86 | events.quitting += on_quitting
87 |
88 | # 接收web端的cmd_queue队列指令消息并派发给相应的slave
89 | def master_cmd(self):
90 | while True:
91 | if not self.cmd_queue.empty():
92 | while not self.cmd_queue.empty():
93 | cmd = self.cmd_queue.get()
94 | logger.info('Master: Get new command - [%s]' % cmd['type'])
95 | if cmd['type'] == 'get_clients':
96 | del_clients = []
97 | client_list = []
98 | for client in six.itervalues(self.clients):
99 | if client.last_time + 61 > time.time():
100 | client_list.append({"id": client.id, "state": client.state, "slave_num": client.slave_num})
101 | else:
102 | del_clients.append(client.id)
103 | for i in del_clients:
104 | print(i)
105 | del self.clients[i]
106 | if client_list:
107 | self.result_queue.put(client_list)
108 | else:
109 | self.result_queue.put('0')
110 | elif cmd['type'] == 'sent_script':
111 | script_data = {"filename": cmd['filename'], "script": cmd['script']}
112 | for client in six.itervalues(self.clients):
113 | self.send(Message("send_script", script_data, cmd['client_id']))
114 | self.result_queue.put("OK")
115 | elif cmd['type'] == 'run':
116 | data = {"filename": cmd['filename'], "num": cmd['num']}
117 | for client in six.itervalues(self.clients):
118 | self.send(Message("run", data, cmd['client_id']))
119 | # if client.id == cmd['client_id']:
120 | # client.state = STATE_RUNNING
121 | elif cmd['type'] == 'stop':
122 | for client in six.itervalues(self.clients):
123 | self.send(Message("stop", None, cmd['client_id']))
124 | # if client.id == cmd['client_id']:
125 | # client.state = STATE_INIT
126 | elif cmd['type'] == 'get_filelist':
127 | for client in six.itervalues(self.clients):
128 | self.send(Message("get_filelist", None, cmd['client_id']))
129 | elif cmd['type'] == 'get_psinfo':
130 | for client in six.itervalues(self.clients):
131 | self.send(Message("get_psinfo", None, cmd['client_id']))
132 | elif cmd['type'] == 'clear_folder':
133 | for client in six.itervalues(self.clients):
134 | self.send(Message("clear_folder", None, cmd['client_id']))
135 | gevent.sleep(1)
136 |
137 | # 接收slave上报的消息并反馈到result_queue
138 | def master_listener(self):
139 | while True:
140 | msg = self.recv()
141 | logger.info('Master: Get new msg from slave - [%s]' % msg.type)
142 | if msg.type == "slave_ready":
143 | id = msg.node_id
144 | self.clients[id] = SlaveNode(id)
145 | self.clients[id].slave_num = msg.data
146 | self.clients[id].last_time = time.time()
147 | if msg.data == 0:
148 | self.clients[id].state = STATE_INIT
149 | else:
150 | self.clients[id].state = STATE_RUNNING
151 | logger.info(
152 | "Client %r reported as ready. Currently %i clients is running; %i clients ready to swarm." % (id, len(self.clients.running), len(self.clients.ready)))
153 | elif msg.type == "quit":
154 | if msg.node_id in self.clients:
155 | del self.clients[msg.node_id]
156 | logger.info("Client %r is exit. Currently %i clients connected." % (msg.node_id, len(self.clients.ready)))
157 | elif msg.type == "file_list":
158 | self.result_queue.put({"client_id": msg.node_id, "file_list": msg.data})
159 | elif msg.type == "slave_num":
160 | self.clients[msg.node_id].slave_num = msg.data
161 | if msg.data == 0:
162 | self.clients[msg.node_id].state = STATE_INIT
163 | self.result_queue.put("None")
164 | else:
165 | self.clients[msg.node_id].state = STATE_RUNNING
166 | self.result_queue.put("OK")
167 | elif msg.type == "psinfo":
168 | self.result_queue.put({"client_id": msg.node_id, "psinfo": msg.data})
169 | elif msg.type == "clear_folder":
170 | self.result_queue.put({"client_id": msg.node_id, "clear_folder": "OK"})
171 |
172 | def quit(self):
173 | for client in six.itervalues(self.clients):
174 | logger.info("Master: send quit message to client - %s" % client.id)
175 | self.send(Message("quit", None, client.id))
176 | self.greenlet.kill(block=True)
177 |
178 |
179 | class SlaveNode(object):
180 | def __init__(self, id, state=STATE_INIT, slave_num=0):
181 | self.id = id
182 | self.state = state
183 | self.slave_num = slave_num
184 | self.last_time = time.time()
185 |
186 |
187 | def shutdown(code=0):
188 | logger.info("Shutting down (exit code %s), bye." % code)
189 | events.quitting.fire()
190 | sys.exit(code)
191 |
192 |
193 | # install SIGTERM handler
194 | def sig_term_handler():
195 | logger.info("Got SIGTERM signal")
196 | shutdown(0)
197 |
198 |
199 | def start(cmd_queue, result_queue):
200 | master_host = "0" # socket.gethostbyname(socket.gethostname())
201 | port = 6666
202 | server = Master(master_host, port, cmd_queue=cmd_queue, result_queue=result_queue)
203 | logger.info('Master is listening at %s:%i' % (master_host, port))
204 | gevent.signal(signal.SIGTERM, sig_term_handler)
205 |
206 | try:
207 | server.greenlet.join()
208 | # gevent.joinall(server.greenlet.greenlets)
209 | code = 0
210 | shutdown(code=code)
211 | except KeyboardInterrupt as e:
212 | shutdown(1)
213 |
214 |
215 | def start_master():
216 | p_master = multiprocessing.Process(target=start, args=(cmd_queue, result_queue, ))
217 | p_master.daemon = True
218 | p_master.start()
219 | return p_master
220 |
221 |
222 | master_server = start_master()
223 |
224 |
225 | if __name__ == '__main__':
226 | print "Please use python slave.py to load client!"
227 |
--------------------------------------------------------------------------------
/hztest/rpc/slave.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | r"""
3 | slave接收来自master的消息指令,在gevent协程worker中进行处理,包括:状态,资源,脚本文件,locust客户端启停,完成后反馈结果给master;
4 | """
5 | import zmq.green as zmq
6 | from protocol import Message
7 | import os
8 | import codecs
9 | from time import time, sleep
10 | import gevent
11 | from gevent.pool import Group
12 | import random
13 | import socket
14 | from hashlib import md5
15 | import events
16 | import logging
17 | import signal
18 | import sys
19 | from subprocess import Popen
20 | import psutil as ps
21 | import shutil
22 | import platform
23 |
24 |
25 | STATE_INIT, STATE_RUNNING, STATE_STOPPED = ["ready", "running", "stopped"]
26 | sh = logging.StreamHandler()
27 | sh.setFormatter(logging.Formatter('[%(asctime)s] - %(message)s'))
28 | logger = logging.getLogger()
29 | logger.addHandler(sh)
30 | logger.setLevel(logging.INFO)
31 |
32 |
33 | class BaseSocket(object):
34 | def send(self, msg):
35 | self.sender.send(msg.serialize())
36 |
37 | def recv(self):
38 | data = self.receiver.recv()
39 | return Message.unserialize(data)
40 |
41 | def noop(self, *args, **kwargs):
42 | """ Used to link() greenlets to in order to be compatible with gevent 1.0 """
43 | pass
44 |
45 |
46 | class Client(BaseSocket):
47 | def __init__(self, host, port):
48 | self.host = host
49 | context = zmq.Context()
50 | self.receiver = context.socket(zmq.PULL)
51 | try:
52 | self.receiver.connect("tcp://%s:%i" % (host, port + 1))
53 | except:
54 | sys.exit(0)
55 | self.sender = context.socket(zmq.PUSH)
56 | try:
57 | self.sender.connect("tcp://%s:%i" % (host, port))
58 | except:
59 | sys.exit(0)
60 |
61 |
62 | # 定义slave类
63 | class Slave(Client):
64 | def __init__(self, *args, **kwargs):
65 | super(Slave, self).__init__(*args, **kwargs)
66 | self.client_id = socket.gethostname() + "_" + md5(
67 | str(time() + random.randint(0, 10000)).encode('utf-8')).hexdigest()
68 | logger.info("Client id:%r" % self.client_id)
69 | self.state = STATE_INIT
70 | self.slave_num = 0
71 | self.file_name = ''
72 | self.cpu_num = ps.cpu_count()
73 | self.processes = []
74 | self.greenlet = Group()
75 | # 加载gevent协程
76 | self.greenlet.spawn(self.worker).link_exception(callback=self.noop)
77 | self.greenlet.spawn(self.ready_loop).link_exception(callback=self.noop)
78 | def on_quitting():
79 | self.send(Message("quit", None, self.client_id))
80 | self.greenlet.kill(block=True)
81 |
82 | events.quitting += on_quitting
83 |
84 | # 消息收发循环,通过gevent协程加载
85 | def worker(self):
86 | while True:
87 | msg = self.recv()
88 | if msg.node_id == self.client_id:
89 | logger.info('Slave: Get new msg from master - [%s]' % msg.type)
90 | # 接收压测脚本保存到./script文件夹中
91 | if msg.type == "send_script":
92 | logger.info("Save script to file...")
93 | if not os.path.exists("./script/"):
94 | os.mkdir("./script/")
95 | self.file_name = os.path.join("./script/", msg.data["filename"])
96 | with codecs.open(self.file_name, 'w', encoding='utf-8') as f:
97 | f.write(msg.data["script"])
98 | logger.info("Script saved into file:%s" % self.file_name)
99 | # 运行locust压测进程,完成后返回成功启动的进程数给master
100 | elif msg.type == "run":
101 | if self.state != STATE_RUNNING:
102 | self.run_locusts(master_host=self.host, nums=msg.data["num"], file_name=msg.data["filename"])
103 | if self.slave_num > 0:
104 | self.state = STATE_RUNNING
105 | logger.info("Client %s run OK!" % self.client_id)
106 | else:
107 | self.state = STATE_INIT
108 | self.send(Message("slave_num", self.slave_num, self.client_id))
109 | # 停止locust压测进程并更新状态给master
110 | elif msg.type == "stop":
111 | logger.info("Client %s stopped!" % self.client_id)
112 | self.stop()
113 | #self.send(Message("client_ready", self.slave_num, self.client_id))
114 | self.send(Message("slave_num", self.slave_num, self.client_id))
115 | # 退出slave,当master退出时收到此消息
116 | elif msg.type == "quit":
117 | logger.info("Got quit message from master, shutting down...")
118 | self.stop()
119 | self.greenlet.kill(block=True)
120 | # 获取当前客户端的压测文件列表
121 | elif msg.type == "get_filelist":
122 | if os.path.exists("./script/"):
123 | file_list = []
124 | for root, dirs, files in os.walk("./script/"):
125 | for f in files:
126 | if os.path.splitext(f)[1] == '.py':
127 | file_list.append(f)
128 | self.send(Message("file_list", file_list, self.client_id))
129 | else:
130 | self.send(Message("file_list", None, self.client_id))
131 | # 获取当前客户端的资源状态:包括IP, CPU,内存,网络
132 | elif msg.type == "get_psinfo":
133 | ip = socket.gethostbyname(socket.gethostname())
134 | nets = ps.net_io_counters()
135 | sleep(1)
136 | nets1 = ps.net_io_counters()
137 | net = {'sent': nets1.bytes_sent / 1024, 'recv': nets1.bytes_recv / 1024,
138 | 'per_sec_sent': (nets1.bytes_sent - nets.bytes_sent) / 1024,
139 | 'per_sec_recv': (nets1.bytes_recv - nets.bytes_recv) / 1024}
140 | cpu_times = ps.cpu_percent(interval=0.1)
141 | cpu_logical_nums = ps.cpu_count()
142 | cpu_nums = ps.cpu_count(logical=False)
143 | cpu_freq = ps.cpu_freq()
144 | if cpu_freq is not None:
145 | cpu = {'num': cpu_nums, 'logical_num': cpu_logical_nums, 'percent': cpu_times,
146 | 'freq': {'current': cpu_freq.current, 'min': cpu_freq.min, 'max': cpu_freq.max}}
147 | else:
148 | cpu = {'num': cpu_nums, 'logical_num': cpu_logical_nums, 'percent': cpu_times,
149 | 'freq': {'current': 0, 'min': 0, 'max': 0}}
150 | mems = ps.virtual_memory()
151 | mem = {'total': mems.total / 1024 / 1024, 'available': mems.available / 1024 / 1024,
152 | 'percent': mems.percent}
153 | psinfo = {"cpu": cpu, "mem": mem, "net": net, "IP": ip}
154 | self.send(Message("psinfo", psinfo, self.client_id))
155 | # 清除压测脚本文件夹
156 | elif msg.type == "clear_folder":
157 | if os.path.exists("./script/"):
158 | shutil.rmtree("./script")
159 | self.send(Message("clear_folder", None, self.client_id))
160 |
161 | # 每分钟向master上报状态
162 | def ready_loop(self):
163 | while True:
164 | # 发送ready状态至master
165 | logger.info('Send ready to server!')
166 | self.send(Message("slave_ready", self.slave_num, self.client_id))
167 | gevent.sleep(60)
168 |
169 | # 退出locust压测进程
170 | def stop(self):
171 | self.state = STATE_STOPPED
172 | self.slave_num = 0
173 | for p in self.processes:
174 | try:
175 | procs = p.children(recursive=True)
176 | for proc in procs:
177 | if platform.system() == "Windows":
178 | proc.send_signal(0)
179 | else:
180 | proc.terminate()
181 | proc.wait()
182 | if platform.system() == "Windows":
183 | p.send_signal(0)
184 | else:
185 | p.terminate()
186 | p.wait()
187 | except:
188 | pass
189 | logger.info("Quit a locust client process!")
190 | self.processes = []
191 |
192 | # 运行locust压测进程
193 | def run_locusts(self, master_host, nums, file_name):
194 | # 设置压测进程数,不大于CPU逻辑核心数
195 | if int(nums) > self.cpu_num or int(nums) < 1:
196 | slave_num = self.cpu_num
197 | else:
198 | slave_num = int(nums)
199 | # 设置各压测进程的压测脚本,如果web端选择的小于进程数,则循环选择
200 | script_file = []
201 | for i in range(slave_num):
202 | script_file.append(os.path.join('./script/', file_name[i % len(file_name)]))
203 | # 启动压测进程
204 | for i in range(slave_num):
205 | cmd = 'locust -f %s --slave --no-reset-stats --master-host=%s' % (script_file[i], master_host)
206 | print cmd
207 | p = ps.Popen(cmd, shell=True, stdout=None, stderr=None)
208 | self.processes.append(p)
209 | sleep(1)
210 | # 更新启动成功的压测进程列表
211 | proc = []
212 | for p in self.processes:
213 | if p.poll() is None:
214 | proc.append(p)
215 | self.processes = proc
216 | self.slave_num = len(proc)
217 |
218 |
219 | def shutdown(code=0):
220 | logger.info("Shutting down (exit code %s), bye." % code)
221 | events.quitting.fire()
222 | sys.exit(code)
223 |
224 |
225 | # install SIGTERM handler
226 | def sig_term_handler():
227 | logger.info("Got SIGTERM signal")
228 | shutdown(0)
229 |
230 |
231 | def slave(host=''):
232 | if host == '':
233 | host = socket.gethostbyname(socket.gethostname())
234 | port = 6666
235 | client = Slave(host, port)
236 | logger.info('Slave is starting at %s:%i' % (host, port))
237 | gevent.signal(signal.SIGTERM, sig_term_handler)
238 | try:
239 | client.greenlet.join()
240 | # gevent.joinall(client.greenlet.greenlets)
241 | code = 0
242 | shutdown(code=code)
243 | except KeyboardInterrupt as e:
244 | shutdown(1)
245 |
246 |
247 | if __name__ == '__main__':
248 | if len(sys.argv) > 1:
249 | slave(sys.argv[1])
250 | else:
251 | slave(socket.gethostbyname(socket.gethostname()))
252 |
253 |
--------------------------------------------------------------------------------
/hztest/views.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | r"""
3 | 本Locust压测调试系统分三块:web,master, slave;
4 | web(views.py): 用于对WEB交互的调试指令进行处理响应,把WEB操作通过命令队列(cmd_queue)发送给master去分发给相应的slave客户端执行,然后通过结果队列(result_queue)接收结果;
5 | master.py: 负责从web接收队列请求,并通过相应的slave处理;
6 | slave.py: 接收master的请求进行处理,包括:状态,资源,脚本文件,locust客户端启停, 并反馈结果;
7 | """
8 |
9 | from django.shortcuts import render
10 | from .rpc.master import cmd_queue, result_queue, lock, logger
11 | import time
12 | import os.path
13 | import os
14 | from subprocess import Popen
15 | import psutil
16 | import platform
17 |
18 | locust_script = './hztest/rpc/script/internal-test.py' # Locust master script file
19 | script_filename = 'internal-test.py' # default script file name of client if don't select file from web
20 | locust_status = 'stop'
21 | p = None
22 |
23 |
24 | # 等待结果消息队列返回数据
25 | def wait_result():
26 | retry = 0
27 | result = None
28 | while result_queue.empty():
29 | time.sleep(1)
30 | retry = retry + 1
31 | if retry > 10:
32 | logger.info('Web server: retry max times, no clients response;')
33 | break
34 | logger.info('Web server: command response is none, get in next 1s...')
35 | while not result_queue.empty():
36 | result = result_queue.get()
37 | return result
38 |
39 |
40 | # 刷新客户端列表
41 | def refresh_clients():
42 | logger.info('Web server: send new command - [get_clients]')
43 | cmd_queue.put({'type': 'get_clients'})
44 | clients = []
45 | q = wait_result()
46 | if q:
47 | if q != '0':
48 | logger.info('Web server: command - [get_clients] - Got response!')
49 | print(q)
50 | clients = q
51 | return {'num': len(clients), 'clients': clients}
52 |
53 |
54 | # 发送压测脚本到客户端
55 | def send_script(client_id, filename, script):
56 | logger.info('Web server: send new command - [send_script(%s)]' % client_id)
57 | cmd_queue.put({'type': 'sent_script', 'client_id': client_id, 'filename': filename, 'script': script})
58 | q = wait_result()
59 | if q:
60 | if q == 'OK':
61 | logger.info('Web server: command - [send_script(%s)] - Got response!' % client_id)
62 | return 'Script send to %s successful!' % client_id
63 | return 'Script send to %s failed!' % client_id
64 |
65 |
66 | # 启动客户端压测进程
67 | def run(client_id, num, filename):
68 | logger.info('Web server: send new command - [run locust slave(%s)]' % client_id)
69 | cmd_queue.put({'type': 'run', 'client_id': client_id, 'filename': filename, 'num': num})
70 | q = wait_result()
71 | if q:
72 | if q == 'OK':
73 | logger.info('Web server: command - [run locust slave(%s)] - Got response!' % client_id)
74 | return 'Client %s run successful!' % client_id
75 | return 'Client %s run failed!' % client_id
76 |
77 |
78 | # 停止客户端压测进程
79 | def stop(client_id):
80 | logger.info('Web server: send new command - [stop locust slave(%s)]' % client_id)
81 | cmd_queue.put({'type': 'stop', 'client_id': client_id})
82 | q = wait_result()
83 | if q:
84 | if q == 'None':
85 | logger.info('Web server: command - [stop locust slave(%s)] - Got response!' % client_id)
86 | return 'Client %s stop successful!' % client_id
87 | return 'Client %s stop failed!' % client_id
88 |
89 |
90 | # 获取客户端压测脚本文件列表
91 | def get_filelist(client_id):
92 | logger.info('Web server: send new command - [get file list(%s)]' % client_id)
93 | cmd_queue.put({'type': 'get_filelist', 'client_id': client_id})
94 | q = wait_result()
95 | if q:
96 | if q['client_id'] == client_id:
97 | if q['file_list']:
98 | logger.info('Web server: command - [get file list(%s)] - Got response!' % client_id)
99 | return {'id': client_id, 'file_list': q['file_list']}
100 | return {'id': client_id, 'file_list': ''}
101 |
102 |
103 | # 获取客户端系统资源状态
104 | def get_psinfo(client_id):
105 | logger.info('Web server: send new command - [get ps info(%s)]' % client_id)
106 | cmd_queue.put({'type': 'get_psinfo', 'client_id': client_id})
107 | q = wait_result()
108 | if q:
109 | if q['client_id'] == client_id:
110 | logger.info('Web server: command - [get ps info(%s)] - Got response!' % client_id)
111 | return {'id': client_id, 'psinfo': q['psinfo']}
112 | return {'id': client_id, 'psinfo': ''}
113 |
114 |
115 | # 清除客户端压测脚本文件夹
116 | def clear_folder(client_id):
117 | logger.info('Web server: send new command - [clear folder(%s)]' % client_id)
118 | cmd_queue.put({'type': 'clear_folder', 'client_id': client_id})
119 | q = wait_result()
120 | if q:
121 | if q['client_id'] == client_id:
122 | if q['clear_folder'] == 'OK':
123 | logger.info('Web server: command - [clear folder(%s)] - Got response!' % client_id)
124 | return 'Client %s script folder cleared!' % client_id
125 | return 'Failed to clear client %s script folder!' % client_id
126 |
127 |
128 | # 参考停止子进程代码,暂时不用
129 | def reap_children(timeout=3):
130 | global p
131 | "Tries hard to terminate and ultimately kill all the children of this process."
132 | def on_terminate(proc):
133 | print("process {} terminated with exit code {}".format(proc, proc.returncode))
134 |
135 | procs = p.children()
136 | # send SIGTERM
137 | for proc in procs:
138 | proc.terminate()
139 | gone, alive = psutil.wait_procs(procs, timeout=timeout, callback=on_terminate)
140 | if alive:
141 | # send SIGKILL
142 | for proc in alive:
143 | print("process {} survived SIGTERM; trying SIGKILL" % p)
144 | proc.kill()
145 | gone, alive = psutil.wait_procs(alive, timeout=timeout, callback=on_terminate)
146 | if alive:
147 | # give up
148 | for proc in alive:
149 | print("process {} survived SIGKILL; giving up" % proc)
150 |
151 |
152 | # Web交互处理
153 | def hello(request):
154 | # 使用进程锁每个WEB请求独占,规避多用户在WEB下操作时冲突
155 | with lock:
156 | global locust_status, p, locust_script, script_filename
157 | context = dict()
158 | context['filelist'] = []
159 | context['hello'] = 'Clients list OK!'
160 | context['script'] = ''
161 | context['text'] = ''
162 | context['clients'] = refresh_clients()
163 | if request.method == 'POST':
164 | print(request.POST)
165 | if context['clients']['num'] > 0:
166 | for client in context['clients']['clients']:
167 | # 启动/停止压测Slave
168 | name = 'run%s' % client['id']
169 | if name in request.POST:
170 | if request.POST.get(name, None) == 'ready':
171 | # 启动客户端压测进程
172 | file_select = 'fileselect%s' % client['id']
173 | num = request.POST.get('num%s' % client['id'], None)
174 | if file_select in request.POST:
175 | file_name = request.POST.getlist(file_select)
176 | else:
177 | # 如果未选择压测脚本,则使用默认脚本名
178 | file_name = [script_filename]
179 | context['hello'] = run(client['id'], num, file_name)
180 | context['clients'] = refresh_clients()
181 | for c in context['clients']['clients']:
182 | if c['id'] == client['id']:
183 | if c['slave_num'] == 0:
184 | context['hello'] = 'Client %s run error, please check you script file %s is valid!' % (client['id'], file_name)
185 | else:
186 | context['hello'] = 'Client %s run OK, script file is %s!' % (client['id'], file_name)
187 | else:
188 | # 停止客户端压测进程
189 | context['hello'] = stop(client['id'])
190 | context['clients'] = refresh_clients()
191 | break
192 | # 获取客户端的脚本文件列表
193 | name = 'filelist%s' % client['id']
194 | if name in request.POST:
195 | file_list = get_filelist(client['id'])
196 | if file_list['id'] == client['id']:
197 | context['filelist'].append(file_list)
198 | break
199 | # 清除客户端脚本文件夹
200 | name = 'clear%s' % client['id']
201 | if name in request.POST:
202 | context['hello'] = clear_folder(client['id'])
203 | break
204 | # 发送压测脚本到客户端
205 | name = 'send%s' % client['id']
206 | if name in request.POST:
207 | if request.FILES.get(client['id'], None):
208 | # 将所选脚本文件内容发给客户端,文件控件可以多选
209 | script_files = request.FILES.getlist(client['id'], None)
210 | for script_file in script_files:
211 | if os.path.splitext(script_file.name)[1] != '.py':
212 | context['hello'] = "File name should be end with .py!"
213 | else:
214 | script = script_file.read()
215 | context['hello'] = send_script(client['id'], script_file.name, script)
216 | elif 'text%s' % client['id'] in request.POST:
217 | # 将页面上编辑后的脚本内容发送给客户端,前提为未选择文件
218 | context['hello'] = send_script(client['id'], request.POST.get('filename', None), request.POST.get('text%s' % client['id'], None))
219 | break
220 | # 编辑压测脚本(如果多选,则只编辑列表中最后一个文件)
221 | name = 'edit%s' % client['id']
222 | if name in request.POST:
223 | if request.FILES.get(client['id'], None):
224 | script_file = request.FILES.get(client['id'], None)
225 | context['script'] = script_file.read()
226 | context['text'] = client['id']
227 | context['filename'] = script_file.name
228 | break
229 |
230 | # 获取客户端系统资源利用率
231 | if request.POST.get('mon_clients', False):
232 | psinfo = []
233 | for c in context['clients']['clients']:
234 | psinfo.append(get_psinfo(c['id']))
235 | context['psinfos'] = psinfo
236 | context['mon_flag'] = "Checked"
237 | context['hello'] = "Clients's monitor data refresh OK!"
238 | else:
239 | context['mon_flag'] = ""
240 |
241 | # 启动/停止Locust Master
242 | if 'start_locust' in request.POST or 'stop_locust' in request.POST:
243 | if locust_status == 'run':
244 | # 停止Locust Master
245 | if p is not None:
246 | logger.info("Server: try to stop the locust master!")
247 | if p.poll() is None:
248 | try:
249 | # 先停止子进程再停止自身进程,Popen在容器中启动会有两个进程,一个是shell进程,另一个是应用进程
250 | procs = p.children(recursive=True)
251 | for proc in procs:
252 | if platform.system() == "Windows":
253 | proc.send_signal(0)
254 | else:
255 | proc.terminate()
256 | proc.wait()
257 | if platform.system() == "Windows":
258 | p.send_signal(0)
259 | else:
260 | p.terminate()
261 | p.wait()
262 | p = None
263 | logger.info("Server: locust master stopped!")
264 | except:
265 | pass
266 | if p is None:
267 | context['hello'] = 'Locust master has been stopped!'
268 | logger.info("Server: locust master is stopped!")
269 | locust_status = 'stop'
270 | # 通知各客户端停止压测
271 | for client in context['clients']['clients']:
272 | stop(client['id'])
273 | context['clients'] = refresh_clients()
274 | else:
275 | # 启动Locust的Master模式
276 | p = psutil.Popen('locust -f %s --master --no-reset-stats' % locust_script, shell=True, stdout=None, stderr=None)
277 | time.sleep(1)
278 | if p.poll() is not None:
279 | # 判断Locust进程是否启动失败并给出提示
280 | if p.poll() != 0:
281 | logger.info("Server: failed to start locus master...")
282 | context['hello'] = 'Failed to start locust master! Please check script file: %s' % locust_script
283 | p = None
284 | else:
285 | # 成功启动Locust的Master模式进程
286 | print("Locust Master process PID:{}".format(p.pid))
287 | logger.info("Server: locust master is running...")
288 | context['hello'] = 'Locust master has been running!'
289 | locust_status = 'run'
290 | context['locust'] = locust_status
291 | host = request.get_host()
292 | context['host'] = host.split(':')[0]
293 | return render(request, 'hello.html', context)
294 |
295 |
296 |
--------------------------------------------------------------------------------