温故而知新,PHP Swoole使用过程中的一些思考

温故而知新

今天突然发现很多以前理解过的概念,再次回顾的时候感觉有点模糊了,翻了翻笔记又有了一些新的理解。

1.场景

  • 简单的异步任务执行可以直接通过管道向子进程投递异步任务,然后触发事件执行任务。
  • Redis的订阅与发布也可以实现上述的功能,但它的优势在于它可以一对多,PUBLISH之后可以同时触发多个订阅的事件,并且可以在任意进程内进行订阅。
  • 专门的消息队列组件,则适用于对消息队列要求比较高的场景(消息确认、消费者组等)

在大多数操作系统中,父进程和子进程通常被分配相同的 CPU 时间片。时间片是操作系统用来调度进程的一种机制,它将 CPU 时间划分为较小的块,并按照某种策略分配给不同的进程。每个进程被分配一个时间片,在该时间片结束后,操作系统会剥夺该进程的 CPU 使用权,并将其分配给其他进程。所以多进程程序获得的CPU时间片比单进程程序多。

为什么不使用管道作为消息队列的通信接口,而是使用Redis List?
处理异步事件的时候,假设只有一个子进程,可以直接使用管理进行通信,然后触发事件。但是一个进程的处理能力是有限的,所以我们往往需要多个进程同时处理异步任务。多个进程同时读取Redis队列时,由于Redis的单线程模型,所以只有一个进程会拿到这个消息

2.问题

  • Mysql一个链接的内存占用?(PHP协程创建了100个不进行操作的连接,占用内存7.8m)
  • 主从复制,数据同步延时的问题?
  • 数据量过大时,避免使用join,通过协程并发查询?
  • 将单个查询分解成多个查询,通过协程并发查询?
  • 读是共享锁,写是排它锁,考虑读写分离?
  • 创建协程,单个协程需要的内存大小(底层默认分配2M(C)虚拟内存+8K(PHP)内存(PHP-7.2或更高版本)。这里的虚拟内存是指操作系统并不会立即分配2M物理内存,系统会根据在内存实际读写时发生缺页中断,再分配实际内存)?
  • 测压过程中如何一步步排查问题所在?
  • 自定义进程的数量,消费者消费的速度、消费者被阻塞的时长?
  • 索引降维和连接池预热?
  • 数据量过大时,直接分库,通过封装中间层,来实现现有业务的无缝迁移。
  • 多个进程分配到的时间片会更多,通过多进程处理异步任务。多进程封装成一个进程组,然后通过原子计数器将任务平均派发到每一个进程。
  • 协程机制的优点之一就是有阻塞就会自动处理别的任务,比如Mysql被阻塞就去处理Redis读取,Redis被阻塞就去Mysql;

3.Swoole协程

  • 协程没有IO等待 正常执行PHP代码,不会产生执行流程切换
  • 协程遇到IO等待 立即将控制权切,待IO完成后,重新将执行流切回原来协程切出的点
  • 协程并行协程依次执行,同上一个逻辑
  • 协程嵌套执行流程由外向内逐层进入,直到发生IO,然后切到外层协程,父协程不会等待子协程结束
  • Swoole的协程在底层实现上是单线程的,因此同一时间只有一个协程在工作,协程的执行是串行的。这与线程不同,多个线程会被操作系统调度到多个CPU并行执行。
  • 一个协程正在运行时,其他协程会停止工作。当前协程执行阻塞IO操作时会挂起,底层调度器会进入事件循环。当有IO完成事件时,底层调度器恢复事件对应的协程的执行。
  • 对CPU多核的利用,仍然依赖于Swoole引擎的多进程机制。
  • Swoole遇到需要等待的IO才会切换,比如SQL查询、比如网络请求等待响应、比如读取文件等找到那个文件。
  • 从Mysql获取返回的结果集,这个需要CPU来计算,需要写内存,所以Swoole协程的单线程模型是串行来的,所以对于结果集数据量级过大的查询,效果不明显。
  • 搞明白什么时候会发生协程切换,业务无关的任务可以异步处理

4.关于队列

队列可以通过客户重要性等条件因素,进行优先级划分。优先级越高的越先处理(Redis的zSet类型数据)。

如何及时处理的区别就在于,队列可以进行中间操作,根据任务进行不同的异步处理。

5.协程的调度

Swoole 的协程化主要集中在 I/O 操作上,通过异步 I/O 提升了 I/O 密集型任务的性能。对于 CPU 密集型操作,虽然不会阻塞协程,但可能会占用较多 CPU 时间,影响协程的响应速度。协程的切换主要依赖于以下几种情况:

  • I/O 操作:当协程执行异步 I/O 操作时,会自动挂起,让出 CPU 给其他协程运行。
  • 显式让出:协程可以通过调用 Swoole\Coroutine::yield 或 Swoole\Coroutine::suspend 显式让出 CPU。
  • 超时等待:协程在等待某个操作完成时,如果设置了超时时间,超时后会自动让出 CPU。

CPU 密集型操作不会触发协程的挂起。如果一个协程执行了耗时 5 秒的CPU 密集型操作,它会占用当前协程的 CPU 时间。具体影响如下:

  • 当前协程:执行CPU 密集型操作的协程会持续占用 CPU,直到操作完成。
  • 其他协程:其他协程仍然可以运行,但会受到当前协程占用 CPU 时间的影响。如果当前协程占用 CPU 时间过长,其他协程的执行时间会相应减少。
swoole.enable_preemptive_scheduler ,可防止某些协程死循环占用 CPU 时间过长 (10ms 的 CPU 时间) 导致其它协程得不到调度

6.一个进程的 CPU 时间由什么决定

一个进程的 CPU 时间主要由以下几个因素决定:
  1. 进程的优先级
    • 操作系统会根据进程的优先级分配 CPU 时间。高优先级的进程会获得更多的 CPU 时间,而低优先级的进程可能会被推迟执行。
  2. 进程的 CPU 使用情况
    • 如果进程执行的是 CPU 密集型任务(如计算密集型操作),它会占用较多的 CPU 时间。
    • 如果进程执行的是 I/O 密集型任务(如文件读写、网络通信),它会在等待 I/O 操作完成时释放 CPU 时间,让其他进程运行。
  3. 操作系统的调度策略
    • 操作系统使用调度算法(如时间片轮转、优先级调度等)来分配 CPU 时间。每个进程会获得一定的时间片(通常几毫秒)来运行,时间片结束后,操作系统会切换到其他进程。
    • 在多核处理器系统中,多个进程可以同时在不同的核心上运行,从而提高系统的整体性能。
  4. 系统负载
    • 当系统负载较高(即有多个进程同时竞争 CPU 资源)时,每个进程获得的 CPU 时间会减少。
    • 当系统负载较低时,进程可能会获得更多的 CPU 时间。
  5. 进程的阻塞状态
    • 如果进程进入阻塞状态(如等待 I/O 操作完成),它会释放 CPU 时间,让其他进程运行。
    • 阻塞状态的进程不会占用 CPU 时间,直到它从阻塞状态中恢复。
进程不能无限申请 CPU 时间
操作系统会根据调度算法和进程的优先级来限制进程的 CPU 使用时间。即使进程试图占用更多的 CPU 时间,操作系统也会通过时间片轮转等方式强制切换到其他进程。

7.常见的进程阻塞

I/O 阻塞是指进程在等待输入/输出操作完成时进入的阻塞状态。常见的 I/O 操作包括:

  • 文件读写:如打开文件、读取文件内容、写入文件等。
  • 网络通信:如发送数据、接收数据等。
  • 设备操作:如读取磁盘数据、等待用户输入等。

进程在等待某个时间间隔时会进入阻塞状态。常见的操作包括:

  • sleep:使进程休眠指定的时间。
  • usleep:使进程休眠指定的微秒数。

......

阻塞卡主,跟CPU占用过高卡主,是两种情况