文章目录
分布式、多线程、高并发
- 分布式是从物理资源的角度去将不同的机器组成一个整体对外服务,技术范围非常广且难度非常大,有了这个基础,高并发、高吞吐等系统很容易构建;
- 高并发是从业务角度去描述系统的能力,实现高并发的手段可以采用分布式,也可以采用诸如缓存、CDN等,当然也包括多线程;
- 多线程则聚焦于如何使用编程语言将CPU调度能力最大化。
分布式
分布式更多的一个概念,是为了解决单个物理服务器容量和性能瓶颈问题而采用的优化手段。
该领域需要解决的问题在不同的技术层面上,又包括:分布式文件系统、分布式缓存、分布式数据库、分布式计算等,一些名词Hadoop、zookeeper、MQ等都跟分布式有关。
从理念上讲,分布式的实现有两种形式:
- 水平扩展:当一台机器扛不住流量时,就通过添加机器的方式,将流量平分到所有服务器上,所有机器都可以提供相当的服务;
- 垂直拆分:前端有多种查询需求时,一台机器扛不住,可以将不同的需求分发到不同的机器上,比如A机器处理余票查询的请求,B机器处理支付的请求。
高并发
相对于分布式来讲,高并发在解决问题上会集中一些,其反应的是同时有多少量:比如在线直播服务,同时与上万人观看。
高并发可以通过分布式技术去解决,将并发流量分到不同的物理服务器上。
但除此之外,还可以有很多其他优化手段:比如使用缓存系统,将所有的吗,静态内容放到CDNA等;还可以使用多线程技术将一台服务器的服务器能量最大化。
在这个‘云’的时代,提高分布式系统并发能力的方式,方法论上主要有两种:垂直扩展(Scale Up)与水平扩展(Scale Out)。
- 垂直扩展单机处理能力。垂直扩展的方式又有两种:
- 增强单机硬件性能,例如增加CPU核数如32核,升级更好的网卡如万兆,升级更好的硬盘如SSD,扩充硬盘容量如2T,扩充系统内存如128G。
- 提升单机架构性能,例如使用Cache来减少I/O次数,使用异步来增加单服务吞吐量,使用无锁数据结构来减少响应时间。
- 水平扩展,只要增加服务器数量,就能线性扩充系统性能。虚拟化技术的出现,让水平扩展变得轻松且简单。现在的云主机几乎是虚拟机,而不是物理主机。这样的话,线性扩充也是分分钟的事,前提是有足够的物理主机支撑。
高并发的三个经典问题:
- 单台服务器最大并发,单台服务器最大并发问题,一般是指台服务器能够支持多少TCP并发连接。一种理论说法是受到端口号范围限制。操作系统上端口号1024以下是系统保留的,从1024-65535是用户使用的。由于每个TCP连接都要占一个端口号,所以我们最多可以有60000多个并发连接。但实际上单击并发连接数肯定要受到硬件资源(内存、网卡)、网络资源(宽带)的限制。特别是网卡处理数据的能力,它是最大并发的瓶颈。
- C10K并发连接问题是指单机1万个并发连接问题。如何突破单机性能局限,是高性能网络编程所必须要直面的问题。这些局限和问题最早被Dan Kegel进行归纳总结,首次成系统的分析和提出解决方案,后来这种普遍的网络现象和技术局限都被大家称为C10K问题。C10K问题本质上是操作系统问题。对于Web1.0/2.0时代的操作系统而言,传统的同步阻塞I/O模型都是一样的,处理的方式都是requests per second,并发10K和100K的区别关键在于CPU。创建的进程线程多了,数据拷贝频繁(缓存I/O、内核将数据拷贝到用户进程空间、阻塞),进程/线程上下文切换消耗大,导致操作系统崩溃,这就是C10K问题的本质。
多线程
多线程是指从软件或者硬件上实现多个线程并发执行的技术,它更多的是解决CPU调度多个进程的问题,从而让这些进程看上去是同时执行(实际是交替运行的)。
在这几个概念中,多线程解决的问题是最明确的,手段也是比较单一的,基本上遇到的最大问题就是线程安全。
Flask
Flask默认是单进程、单线程阻塞的任务模式,在项目上线的时候可以通过nginx+gunicorn的方式部署flask任务,但是在开发的过程中如果想通过延迟的方式测试高并发,需要在app.run()中通过threaded和processes开启线程支持和进程支持。
但是多进程或多线程只能选择一个,不能同时开启。
- threaded 多线程支持,默认为False,即不开启多线程。
- processes 进程数量,默认为1.
app.run(host='0.0.0.0', port=4000, threaded=True, processes=1)
gunicorn 部署Flask项目
使用flask自带的服务器,可以完成web服务的启动。在生产环境中flask自带的服务器,无法满足性能需求,需要采用Gunicorn做wsgi容器,来部署flask程序。Gunicorn(绿色独角兽)是一个Python WSGI的HTTP服务器。从Ruby的独角兽(Unicorn)项目移植。该Gunicorn服务器与各种Web框架兼容,实现非常简单,轻量级的资源消耗。
WSGI(Web Server Gateway Interface)web服务器网关接口,是一种规范,它是web服务器和web应用程序之间的接口。它的作用就像是桥梁,连接在web服务器和web应用框架之间。
Gunicorn是基于pre-fork模型的,有一个中心管理进程(master process)用来管理worker进程集合。Master从不知道任何关于客户端的信息。所有的请求和响应处理都是由worker进程来处理的。
主程序是一个简单的循环,监听各种信号以及相应的响应进程。master管理着正在运行的worker集合,通过监听各种信号比如TTIN,TTOU和CHLD。TTIN和TTOU响应的增加和减少worker的数目。CHLD信号表明一个子进程已经结束了,在这种情况下master会自动的重启失败的worker。
gunicorn架构
gunicorn实现了一个Unix的预分发web的服务端。
- gunicorn 启动会被分发到一个主线程,然后产生的子线程就是对应的worker。
- 主线程的作用是确保worker数量与设置中定义的数量相同。如果任何一个worker挂掉,主线程都可以通过分发它自身而另行启动。
- worker的角色是处理HTTP请求。
- 预分发就意味着主线程在处理HTTP请求之前就创建好了worker。
- 操作系统的内核就负责处理worker进程之间的负载均衡。
为了提高使用gunicorn时的性能,需理解3种并发方式。
- worker模式(又成Unix进程模式)
每个worker都是一个加载Python应用程序的unix进程。worker之间没有共享内存。这时候建议的workers数量是(2*CPU)+1。
- 多线程
Gunicorn 还允许每个 worker 拥有多个线程。在这种场景下,Python 应用程序每个 worker 都会加载一次,同一个 worker 生成的每个线程共享相同的内存空间。
- 伪线程
有一些 Python 库比如(gevent 和 Asyncio)可以在 Python 中启用多并发。那是基于协程实现的“伪线程”。Gunicrn 允许通过设置对应的 worker 类来使用这些异步 Python 库。
并发 vs. 并行
- 并发是指同时执行 2 个或更多任务,这可能意味着其中只有一个正在处理,而其他的处于暂停状态。
- 并行是指两个或多个任务正在同时执行。
在 Python 中,线程和伪线程都是并发的一种方式,但并不是并行的。但是 workers 是一系列基于并发或者并行的方式。
安装gunicorn
pip3 install gunicorn --user
启动gunicorn
- 最简单方式,默认监听127.0.0.1:8000
gunicorn 入口文件名:app
- 处理高并发需要开多个进程和修改监听端口号
gunicorn -w 4 -b 0.0.0.0:8000 入口文件名:app
-
后台执行方式启动服务
- 方法一
nohup 启动服务的命令 &
nohup gunicorn -w 4 -b 0.0.0.0:8000 入口文件名:app &
- 方法二
在gunicorn的配置文件中零daemon参数为True#gunicorn_config.py daemon = True reload =True #修改程序代码,不用每次都通过gunicorn进行重启
- 方法一
-
查看程序是否运行起来,如果运行起来会有两个进程
fuser -v -n tcp 端口号
-
结束进程
kill -9 进程号
gunicorn配置文件
# coding=utf-8
# /usr/bin/env python
"""
Author: buty
Date: 2019/9/17 下午1:36
Description: gunicorn config
"""
import gevent.monkey
gevent.monkey.patch_all()
import multiprocessing
#server socket
bind = '0.0.0.0:4000'
#worker
# workers = multiprocessing.cpu_count() * 2 + 1
workers = 4
threads = 1
# worker_class = 'gunicorn.workers.gevent.GeventWorker'
# worker_class = 'gthread'#gevent
worker_class = 'sync'
debug = True
# daemon = True
worker_connections = 100
#log
pidfile = 'log/gunicorn.pid'
accesslog = 'log/gunicorn_access.log'
# errorlog = 'log/gunicorn_error.log'
loglevel = 'debug'
access_log_format = '%(h)s %(l)s %(u)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
与logging自带log记录合并,减少文件数量。
# !/usr/bin/env python
# -*- coding: utf-8 -*-
#
# @Author: Buty
# @Date : 11/17/21 9:09 AM
# @File : config.py
# config.py
import os
import gevent.monkey
gevent.monkey.patch_all()
import multiprocessing
debug = True
bind = '0.0.0.0:8000'
pidfile = 'log/gunicorn.pid'
# accesslog = 'log/access.log'
# errorlog = 'log/error.log'
logconfig_dict = {
'version':1,
'disable_existing_loggers': False,
'loggers':{
"gunicorn.error": {
"level": "WARNING",# 打日志的等级可以换的,下面的同理
"handlers": ["error_file"], # 对应下面的键
"propagate": 1,
"qualname": "gunicorn.error"
},
"gunicorn.access": {
"level": "DEBUG",
"handlers": ["access_file"],
"propagate": 0,
"qualname": "gunicorn.access"
}
},
'handlers':{
"error_file": {
"class": "logging.handlers.RotatingFileHandler",
"maxBytes": 1024*1024*1024,# 打日志的大小,我这种写法是1个G
"backupCount": 1,# 备份多少份,经过测试,最少也要写1,不然控制不住大小
"formatter": "generic",# 对应下面的键
# 'mode': 'w+',
"filename": "./log/gunicorn.error.log"# 打日志的路径
},
"access_file": {
"class": "logging.handlers.RotatingFileHandler",
"maxBytes": 1024*1024*1024,
"backupCount": 1,
"formatter": "generic",
"filename": "./log/gunicorn.access.log",
}
},
'formatters':{
"generic": {
"format": "'[%(process)d] [%(asctime)s] %(levelname)s [%(filename)s:%(lineno)s] %(message)s'", # 打日志的格式
"datefmt": "[%Y-%m-%d %H:%M:%S %z]",# 时间显示方法
"class": "logging.Formatter"
},
"access": {
"format": "'[%(process)d] [%(asctime)s] %(levelname)s [%(filename)s:%(lineno)s] %(message)s'",
"class": "logging.Formatter"
}
}
}
capture_output = True
loglevel = 'warning'
daemon = True #后台启动
reload = True
#workers = multiprocessing.cpu_count()
workers = 1
worker_class = 'gevent'
x_forwarded_for_header = 'X-FORWARDED-FOR'
# 添加配置文件后的启动方式
gunicorn -c gun.py 入口文件名:app
列出所有进程
pstree -ap|grep gunicorn
最小的一级是worker进程,它们的上一级是gunicorn进程。使用kill -HUP [gunicorn 进程ID]
可以杀掉进程。如果该进程还存在上一级进程,使用kill -9 [进程ID]
将其彻底关闭。
导出测试结果报告
- 保存View Results Tree(查看结果树)结果
- 生成HTML报告
- 点击index.html即可查看结果
报告结构
报告总体分为Dashboard和Charts两部分
Dashboard
- Test and Report informations(测试和报告信息):测试结果保存文件/测试开始时间/测试结束时间/展示过滤器。
- APDEX(Application Performance Index):应用程序性能满意度的标准,范围在0-1之间,1表示达到所有用户均满意,可以在配置文件设置。
- Request Summary:请求的通过率(OK)与失败率(KO),百分比显示。
- Statistics:数据分析,基本将Summary Report和Aggregate Report的结果合并。
- Errors: 错误情况,依据不同的错误类型,将所有错误结果展示。
- Top 5 Errors by sampler:Top5错误信息采样。
Chart
Chart分为三大模块:时间维度信息(Over Time)、吞吐量(Throughput)、响应时间(Response Times)。
Over Time
- Response Times Over Time脚本运行时间内响应时间分布曲线
- Response Time Percentiles Over Time (successful responses) 脚本运行时间内成功响应的请求,响应时间百分位
- Active Threads Over Time 脚本运行时间内的活动线程分布
- Bytes Throughput Over Time脚本运行时间内的吞吐量,单位是byte
- Latencies Over Time脚本运行时间内毫秒级的响应延时
- Connect Time Over Time脚本运行时间内平均连接时间
Throughput
- Hits Per Second (excluding embedded resources) 每秒点击数曲线
- Codes Per Second (excluding embedded resources)每秒状态码分布曲线
- Transactions Per Second 每秒事物数曲线
- Total Transactions Per Second
- Response Time Vs Request 响应时间中值与每秒请求数关系曲线
- Latency Vs Request 延迟时间中值与每秒请求数关系曲线
Response Times
- Response Time Percentiles毫秒级百分位响应时间曲线
- Response Time Overview 响应时间概述柱状图
- Time Vs Threads 活动线程与平均响应时间变化曲线
- Response Time Distribution响应时间分布图
参考资料
[1] Flask 开启多进程或多线程
[2] 使用gunicorn部署Flask项目
[3] nginx+Gunicorn部署Flask项目
[4] 部署flask
[5] flask gunicorn gevent部署
[6] gunicorn 实现 gevent 多线程
[7] gunicorn + flask 异步方案采坑记录
[8] Flask+Gunicorn+Nginx配置多个app
[9] Gunicorn-配置详解
[10] Gunicorn-配置详解
[11] [译] 通过优化 Gunicorn 配置提高性能
[12] A scalable Keras + deep learning REST API
[13] JMeter:生成漂亮的多维度的HTML报告
[14] jmeter5.1.1新玩物:Generate HTML Report(生成HTML报告)
[15] 周杰伦新歌《说好不哭》上线,程序员哭了…
[16] 基于Flask Web框架提供Pytorch 模型在线服务
[17] Flask—学习笔记
[18] 分布式、多线程、高并发都不懂,拿什么去跳槽