关于 Nginx + PHP-FPM 模式下的业务 API 接口,探究并发与并行?

Ref

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
  • 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 批量录入学生成绩接口,参数为 nameage,希望接口每次先查询 Mysqlstudent 表中,是否存在 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

    • 表结构,可以理解为只有 idnameage 三个字段
      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_processesworker_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 协议》,转载必须注明作者和本文链接
明天我们吃什么 悲哀藏在现实中 Tacks
讨论数量: 4

并发 有两层理解,一层是在应用层面,象你的程序,可以同时接受多个人的请求,多个都可以得到处理,每个人的请求是 独立 的,逻辑上不会互相影响。另一层在进程层面,一个 web 请求过来之后,可以做多线程,多个线程同时或依序启动,此时至少是 并发 的,然而这多个线程之间,一个时刻只有一个线程在工作,通过不同切换线程来“看起来”是多个线程共同进行的。如果多个线程能够同时启动,并且同一个时刻有多个线程在工作,那么,就认为是并行 。这也就是为什么并行需要多核。
换个理解就是,在学校的操场上,你一个人,一会儿在跑道1跑步,一会儿跳到跑道2,一会儿又回到跑道1,注意你在跑道1,2上都必须从起点跑完全程。那就是并发。如果跑道1上一个人,跑道2上也有一个人,两个人一起往前冲,那就是并行。
PHP 并不是一门系统为主的语言,所以需要并发并行的机会并不多,并发通过 nginx php-fpm 之类的来解决。并行几乎没有必需的场景所以也没有什么好的方案。
回到主题,你提到的既并发 也不是并发,但几乎可以肯定不是并行.
需要了解更多的并发并行,可以参考一些系统编程语言,比如 c++ golang rust 这些对于并发并行都控制得比较好。

1年前 评论
Tacks (楼主) 11个月前

其他思考

  • ① 并行是跟机器核数配置强相关。如果是多核CPU,那么Nginx+PHP-FPM,其实就是多进程在多核上跑。那么自然提升机器的核心数,也能提升整体接口的并发量,但是效果应该是跟核数的倍数相关,而且为了提升并发量,通过这种方式显然并不是高效的,通常来说 2核2GB 要比 两台1核1GB贵一些。

  • ② 在 Nginx + PHP-FPM 这种模式下,一个简单的 echo 'hello world'; 的接口,无框架,原生PHP,支持并发性能到达该机器的最大上限时,如何考虑再提升并发能力;一方面可能是硬件资源,提升单机的核数和内存或者堆多台机器做负载均衡,另一方面考虑类似 Swoole/WorkermanPHP-CLI 模式;

11个月前 评论

PHP 并不是一门系统语言,所以了解相关概念即可。没有必要非拿 php 来做多线程并行。
就象你可以用瑞士军刀来拧螺丝,但没有必要非拿方天画戟来拧螺丝,虽然也能用,但没必要不是。

11个月前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!