关于 Nginx + PHP-FPM 模式下的业务 API 接口,探究并发与并行?
Ref
- Linux file-max
- Linux
taskset
命令 - PHP 多进程
pcntl
- PHP 多线程
parallel
0、引入问题
- 引入问题: Nginx + PHP-FPM 下,一个批量录入数据接口,发生录入 Mysql 相同的数据(希望相同数据只会录入一次),是理解成并发还是并行?
大致问题:一个 PHP 接口,批量录入数据,MySQL 未建立唯一索引,依靠 PHP 每次先 select 查询数据是否存在,然后再 insert 录入。在大量并发请求下,是否会出现数据录入重复的情况?目前我的测试来看,是会出现数据录入重复的,因为没有唯一索引,也没有用锁来防止同时插入一样的数据。另外想探究一下,并发 与 并行 的区别,如果插入了一样的数据,就表示在代码层面,select 某个数据都是返回不存在的场景,因此两个进程同时进入 insert的,那么这种情况是 两个进程同时执行 insert sql 语句,这种是叫做 并发,还是并行?
既然是探究,就不保证本文说的一些结论是完全正确的,欢迎一起探讨!
1、问题复现
1.1 环境:
- Linux
CentOS Linux release 7.5
- Linux System Max File Descriptor
cat /proc/sys/fs/file-max
=2259544
(Linux 内核级别强制执行的最大文件描述符 (FD)) - Linux Shell Process Max File Descriptor
ulimit -n
=327680
- Linux Memory
32GB
- Linux Cpu
cat /proc/cpuinfo| grep "processor"| wc -l
16
个
- Linux System Max File Descriptor
- Nginx
1.20.1
- php
7.3.33
- mysql
8.0
nginx
一些配置worker_processes 16; # Nginx Worker 进程个数 worker_cpu_affinity auto; # CPU 亲和性 worker_rlimit_nofile 300000; # Worker 工作进程能够打开的最大文件描述符数量 events { worker_connections 20480; # 设置每个 worker 进程的最大并发连接数为 20480 use epoll; # 使用 Linux 下的 epoll I/O 模型 accept_mutex off; # 关闭 accept 锁。accept 锁是 Nginx 中用于解决惊群问题(多个进程同时等待同一个事件)的机制,但在高并发场景下会降低系统的性能,因此可以关闭该锁 multi_accept on; # 允许 worker 进程同时处理多个新连接,在高并发场景下可以减少连接建立的延迟。 }
php-fpm
一些配置pm = static pm.max_children = 500
1.2 需求:
例如一个 http://localhost/api/student/multiadd
批量录入学生成绩接口,参数为 name
和 age
,希望接口每次先查询 Mysql
的 student
表中,是否存在 name
,如果存在就直接返回记录,如果不存在就插入数据库。参数如下。
- json.txt
[
{
"name": "tacks",
"age": 18
},
{
"name": "zhangsan",
"age": 19
},
{
"name": "lisi",
"age": 20
},
...
]
1.3 业务处理:
这里的两种做法,事务在 while 循环外侧,事务在 while 循环内部,各有利弊;
事务在外侧
将事务放在循环外部则只需要建立一次数据库连接,开启一次事务,执行多条 SQL 语句,直到循环结束再提交事务,这样可以大大减少数据库的负载和系统的开销,提高系统的性能和稳定性。
这种应对高并发场景可能更合适。要么都成功,要么都失败。事务在内测
将事务放在循环里面的做法,每次循环都会建立一次数据库连接,开启一次事务,执行一次 SQL 语句,这样频繁的开启和提交事务会造成系统负载的增加,降低系统的性能和稳定性。
这种可能为了数据的准确性。一批数据,可以部分成功,部分失败。
做法1
- Start
verify()
参数校验beginTransaction()
开启Mysql
事务while($students)
遍历批量录入的学生列表,比如一次批量录入 100 条select * from student where name="zhangtsan"
查询单个学生姓名是否录入过- 录入过,记录学生
id
- 未录入,插入学生信息
insert
- 录入过,记录学生
commit()
提交Mysql
事务- DONE
做法2
- Start
verify()
参数校验while($students)
遍历批量录入的学生列表,比如一次批量录入 100 条beginTransaction()
开启Mysql
事务select * from student where name="zhangtsan"
查询单个学生姓名是否录入过- 录入过,记录学生
id
- 未录入,插入学生信息
insert
- 录入过,记录学生
commit()
提交Mysql
事务
- DONE
1.4 限制
Mysql 未对
student
表的name
做唯一主键限制,所以只有php
每次先select
查询是否存在某个学生记录来防止数据录入重复的场景;Mysql
student
- 表结构,可以理解为只有
id
、name
、age
三个字段CREATE TABLE IF NOT EXISTS `student` ( `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, `name` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '姓名' , `age` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '年龄', PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB CHARSET='utf8mb4' COLLATE='utf8mb4_unicode_ci' COMMENT='学生表' ;
- 表结构,可以理解为只有
Mysql
事务隔离级别
mysql> select @@global.transaction_isolation,@@transaction_isolation;
@@global.transaction_isolation @@transaction_isolation
REPEATABLE-READ REPEATABLE-READ
1.5 发生数据录入重复
- 利用压测,在
API
多次相同数据后,发生MySQL
中出现name
重复的记录
ab -n 10000 -c 100 -p json.txt -T "application/json" -H "Content-Type: application/json" "http://localhost/api/student/multiadd"
TIPS:ab 模拟请求的时候,新创建一个 student 表,无数据,然后利用 ab 压测,此时发现是有数据录入的,但是我在 ab 没有压测结束的时候,把数据库表数据清空,然后等待 ab 压测结束,发现大量重复 name 的数据。(疑惑???)
2、并发与并行
2.1 并发&并行的概念
『并发』(Concurrency
) ,是指两个或多个事件在同一时间间隔内发生,强调同一个时间段应对多件事情的能力。
『并行』(Parallel
) ,是指两个或多个事件在同一时刻发生,强调同时处理多件事情的能力,必须有多个 CPU 下。
- 单核心并发
- 在单处理机系统中,同一时刻仅能有一道程序执行,故微观上这些程序只能是分时地快速交替执行,使得在宏观上具有多个任务同时执行的效果;
- 多核心并发
- 单核心 => N 核心
- 同时进行的任务 M 个 ,M 大小的线程队列,交给 N 个 CPU 核心执行, 处理模型为
M:N
- 并行和并发都有体现
- 整体 M 个任务为 并发处理
- 实际上同一个时刻只有 N 个任务可以被同时 并行执行
- 多核心并行
- 当 CPU 核心增多到 N 时,那么同一时间就能有 N 个任务被执行, 重点是同时执行
2.2 并行CPU的个数以什么为准
- 如下所示,可得机器的 物理CPU 16 个;
# 物理CPU个数
[root@Centos7 ~]# lscpu | awk '/^CPU\(s\)/ { print $2 }'
16
# 逻辑CPU个数
[root@Centos7 ~]# lscpu | awk '/^Socket\(s\)/ { print $2 }'
2
# 是否开启超线程 1未开启,大于1开启
[root@Centos7 ~]# lscpu| grep Thread
Thread(s) per core: 1
Nginx 通过 worker_processes
和 worker_cpu_affinity
可以利用CPU多核的能力;
worker_processes
- 用于设置 Nginx 工作进程的数量,通常设置为 CPU 内核数的两倍或四倍。
worker_cpu_affinity
- 用于设置每个工作进程绑定到哪些 CPU 核心上运行。
- auto 设置为自动绑定;
3、我的一些疑惑
3.1 一般为什么接口只提并发量而不讨论并行?
个人拙见:并行一般需要依靠硬件来支持,成本较高;并发更能提高系统整体并发能力,例如缓存、IO复用、负载均衡、多进程、多线程、协程等等;
3.2 什么时候需要考虑并行,PHP中是否有并行的场景?
个人拙见:CPU密集型,可以考虑;但是大多数接口都是IO密集型,读取磁盘、网络调用之类的;PHP如果不讨论多线程技术,就从多进程角度来看,你可以
pcntl_fork()
多个进程, 每个进程分配在不同的 CPU 上执行是操作系统分配的,taskset
命令也可以绑定进程和CPU的亲和力。
本作品采用《CC 协议》,转载必须注明作者和本文链接
并发
有两层理解,一层是在应用层面,象你的程序,可以同时接受多个人的请求,多个都可以得到处理,每个人的请求是独立
的,逻辑上不会互相影响。另一层在进程层面,一个 web 请求过来之后,可以做多线程,多个线程同时或依序启动,此时至少是并发
的,然而这多个线程之间,一个时刻只有一个线程在工作,通过不同切换线程来“看起来”是多个线程共同进行的。如果多个线程能够同时启动,并且同一个时刻有多个线程在工作,那么,就认为是并行
。这也就是为什么并行需要多核。换个理解就是,在学校的操场上,你一个人,一会儿在跑道1跑步,一会儿跳到跑道2,一会儿又回到跑道1,注意你在跑道1,2上都必须从起点跑完全程。那就是并发。如果跑道1上一个人,跑道2上也有一个人,两个人一起往前冲,那就是并行。
PHP
并不是一门系统为主的语言,所以需要并发并行的机会并不多,并发通过 nginx php-fpm 之类的来解决。并行几乎没有必需的场景所以也没有什么好的方案。回到主题,你提到的既
是
并发 也不是
并发,但几乎可以肯定不是
并行.需要了解更多的并发并行,可以参考一些系统编程语言,比如 c++ golang rust 这些对于并发并行都控制得比较好。
其他思考
① 并行是跟机器核数配置强相关。如果是多核CPU,那么
Nginx
+PHP-FPM
,其实就是多进程在多核上跑。那么自然提升机器的核心数,也能提升整体接口的并发量,但是效果应该是跟核数的倍数相关,而且为了提升并发量,通过这种方式显然并不是高效的,通常来说 2核2GB 要比 两台1核1GB贵一些。② 在
Nginx
+PHP-FPM
这种模式下,一个简单的echo 'hello world';
的接口,无框架,原生PHP,支持并发性能到达该机器的最大上限时,如何考虑再提升并发能力;一方面可能是硬件资源,提升单机的核数和内存或者堆多台机器做负载均衡,另一方面考虑类似Swoole
/Workerman
的PHP-CLI
模式;PHP
并不是一门系统语言,所以了解相关概念即可。没有必要非拿php
来做多线程并行。就象你可以用瑞士军刀来拧螺丝,但没有必要非拿方天画戟来拧螺丝,虽然也能用,但没必要不是。