您想了解的有关查询处理的所有信息,但不好意思问

什么是网络服务? 这是一个程序,它通过网络接受传入的请求并对其进行处理,并可能返回响应。


网络服务在很多方面彼此不同。 在本文中,我重点介绍如何处理传入的请求。


选择请求处理方法会产生深远的影响。 如何使同时具有100,000个连接的聊天服务? 采取哪种方法从结构不良的文件流中提取数据? 错误的选择将浪费时间和精力。


本文讨论了诸如进程/线程池,面向事件的处理,半同步/半异步模式等方法。 给出了许多示例,介绍了方法的优缺点,其功能和应用。


引言


查询处理方法的主题并不新鲜,例如,参见: 。 但是,大多数文章仅部分考虑它。 本文旨在填补空白,并提供一致的问题说明。


将考虑以下方法:


  • 顺序处理
  • 请求流程
  • 请求流
  • 进程/线程池
  • 面向事件的处理(反应堆模式)
  • 半同步/半异步模式
  • 输送机加工

应当注意,处理请求的服务不一定是网络服务。 这可能是一项从数据库或任务队列接收新任务的服务。 在本文中,网络服务是指的,但您需要了解所考虑的方法的范围更广。


TL; DR


本文结尾处是一个列表,其中简要介绍了每种方法。


顺序处理


一个应用程序由一个进程中的一个线程组成。 所有请求仅按顺序处理。 没有并行性。 如果多个请求同时到达服务,则其中一个请求将被处理,其余请求将排队。


另外,此方法易于实现。 没有锁定和资源竞争。 明显的缺点是无法扩展大量客户。


申请流程


应用程序由一个核心流程组成,该核心流程接受传入的请求和工作流。 对于每个新请求,主流程都会创建一个处理该请求的工作流程。 按请求数量缩放很简单:每个请求都有自己的进程。


这种架构没有什么复杂的,但是它具有 问题 局限性


  • 该过程消耗大量资源。
    尝试创建与PostgreSQL RDBMS的10,000个并发连接,然后查看结果。
  • 进程没有共享内存(默认)。 如果您需要访问共享数据或共享缓存,则必须映射共享内存(调用linux mmap,munmap)或使用外部存储(memcahed,redis)

这些问题绝不会停止。 下面将显示如何在PostgeSQL RDBMS中对其进行管理。


这种架构的优点


  • 其中一个进程的崩溃不会影响其他进程。 例如,极少数情况下的处理错误不会删除整个应用程序,只会处理已处理的请求
  • 在操作系统级别区分访问权限。 由于该过程是操作系统的本质,因此您可以使用其标准机制来划定对OS资源的访问权限
  • 您可以随时更改运行过程。 例如,如果使用单独的脚本来处理请求,然后替换处理算法,则只需更改脚本即可。 下面将考虑一个示例。
  • 有效使用多核计算机

范例:


  • PostgreSQL RDBMS为每个新连接创建一个新进程。 共享内存用于处理常规数据。 PostgreSQL可以通过许多不同的方式处理进程的高资源消耗。 如果客户很少(专门为分析师服务的客户),那么就没有这种问题。 如果只有一个应用程序可以访问数据库,则可以在应用程序级别创建数据库连接池。 如果应用程序很多,可以使用pgbouncer
  • sshd侦听端口22上的传入请求,并在每个连接上派生。 每个ssh连接都是sshd守护程序的一个分支,该守护程序按顺序接收和执行用户命令。 得益于此架构,操作系统本身的资源可用于区分访问权限
  • 我们自己的实践中的一个例子。 有一些非结构化文件流,您需要从中获取元数据。 主服务进程在处理程序进程之间分发文件。 每个处理程序进程都是一个脚本,它将文件路径作为参数。 文件处理在单独的进程中进行,因此,由于处理错误,整个服务不会崩溃。 要更新处理算法,只需在不停止服务的情况下更改处理脚本即可。

总的来说,我必须说这种方法有其优点,这决定了它的范围,但是可伸缩性非常有限。


请求流


这种方法很像以前的方法。 区别在于使用线程代替进程。 这使您可以立即使用共享内存。 但是,以前的方法的其他优点不再可用,而资源消耗也将很高。


优点:


  • 开箱即用的共享内存
  • 易于实施
  • 高效使用多核CPU

缺点:


  • 流消耗大量资源。 在类似Unix的操作系统上,线程消耗的资源几乎与进程消耗的资源一样多

使用的一个示例是MySQL。 但应注意,MySQL使用混合方法,因此下一节将讨论此示例。


进程/线程池


流(流程)创建起来耗时又长。 为了不浪费资源,您可以重复使用同一线程。 进一步限制了最大线程数后,我们获得了一个线程池(进程)。 现在,主线程接受传入的请求并将其放入队列中。 工作流从队列中获取请求并进行处理。 这种方法可以看作是对请求进行顺序处理的自然扩展:每个工作线程只能按顺序处理流,将它们合并可让您并行处理请求。 如果每个流可以处理1000 rps,那么5个流将处理接近5000 rps的负载(对共享资源的竞争最少)。


该池可以在服务启动时预先创建,也可以逐渐形成。 使用线程池更为常见,因为 允许您应用共享内存。


线程池的大小不必受到限制。 服务可以使用池中的空闲线程,如果没有空闲线程,请创建一个新线程。 处理完请求后,线程将加入池中并等待下一个请求。 此选项是请求线程方法和线程池的组合。 下面将给出一个例子。


优点:


  • 使用许多CPU内核
  • 减少创建线程/进程的成本

缺点:


  • 并发客户端数量有限的可伸缩性。 使用池使我们可以多次重用同一线程,而不会产生额外的资源成本,但是,它不能解决线程/进程消耗大量资源的根本问题。 使用这种方法创建可以同时支持100,000个连接的聊天服务将失败。
  • 可伸缩性受到共享资源的限制,例如,如果线程通过使用信号量/互斥锁来调整对共享内存的访问来使用共享内存。 这是使用共享资源的所有方法的局限性。

范例:


  1. 使用uWSGI和nginx运行的Python应用程序。 uWSGI主进程从nginx接收传入的请求,并将它们分配在处理请求的解释器的Python进程之间。 可以在任何与uWSGI兼容的框架(Django,Flask等)上编写应用程序。
  2. MySQL使用线程池:每个新连接都由池中的空闲线程之一处理。 如果没有空闲线程,则MySQL创建一个新线程。 可用线程池的大小和最大线程(连接)数受设置限制。

如果不是最常见的话,这也许是构建网络服务的最常见方法之一。 它使您可以很好地扩展,达到较大的rps。 该方法的主要限制是同时处理的网络连接数。 实际上,这种方法仅在请求短或客户少的情况下才有效。


面向事件的处理(反应堆模式)


同步和异步这两个范例是彼此永恒的竞争者。 到目前为止,仅讨论了同步方法,但是忽略异步方法是错误的。 面向事件或响应式请求处理是一种方法,其中每个IO操作都是异步执行的,并在操作结束时调用处理程序。 通常,每个请求的处理都包含许多异步调用,然后执行处理程序。 在任何给定时刻,单线程应用程序仅执行一个处理程序的代码,但是各种请求的处理程序的执行彼此交替,这使您可以同时(伪并行)处理许多并行请求。


对这种方法的完整讨论超出了本文的范围。 为了更深入地了解,您可以推荐Reactor(Reactor)NodeJS速度的秘密是什么?在NGINX内部 。 在这里,我们仅限于考虑这种方法的利弊。


优点:


  • 通过rps和同时连接数进行有效缩放。 如果大多数连接正在等待I / O完成,则反应式服务可以同时处理大量连接(数万个)

缺点:


  • 开发的复杂性。 异步风格的编程比同步风格的编程更难。 与同步代码相比,请求处理的逻辑更加复杂,调试也更加困难。
  • 错误导致阻塞整个服务。 如果语言或运行时不是最初为异步处理而设计的,则单个同步操作可能会阻塞整个服务,从而无法扩展。
  • 难以跨CPU内核扩展。 这种方法假定在单个进程中使用一个线程,因此您不能同时使用多个CPU内核。 应当指出,有一些方法可以解决此限制。
  • 上一段的推论:对于需要CPU的请求,此方法不能很好地扩展。 此方法的rps数量与处理每个请求所需的CPU操作数量成反比。 对CPU请求的要求否定了这种方法的优势。

范例:


  1. Node.js使用现成的反应堆模式。 有关更多详细信息,请参见NodeJS速度的秘密是什么?
  2. nginx:nginx的工作进程使用反应器模式来并行处理请求。 有关更多详细信息,请参见Inside NGINX。
  3. 直接使用OS工具(Linux上的epoll,Windows上的IOCP,FreeBSD上的kqueue)或使用框架(libev,libevent,libuv等)的C / C ++程序。

半同步/半异步


该名称取自POSA:并发和联网对象的模式 。 最初,这种模式的解释非常广泛,但是出于本文的目的,我将更狭义地理解这种模式。 半同步/半异步是一种请求处理方法,它对每个请求使用轻量级控制流(绿色线程)。 一个程序在操作系统级别由一个或多个线程组成,但是,该程序执行系统支持OS无法看到且无法控制的绿色线程。


一些例子可以使考虑更加具体:


  1. Go语言服务。 Go语言支持许多轻量级执行线程-goroutine。 该程序使用一个或多个OS线程,但是程序员使用goroutine进行操作,这些goroutine在OS线程之间透明地分布,以便使用多核CPU。
  2. 带gevent库的Python服务。 gevent库允许程序员在库级别使用绿色线程。 整个程序在单个OS线程中执行。

本质上,该方法旨在将异步方法的高性能与对同步代码进行编程的简单性相结合。


使用这种方法,尽管有同步的幻想,程序仍将异步工作:程序执行系统将控制事件循环,并且每个“同步”操作实际上都是异步的。 当调用此类操作时,执行系统将使用OS工具调用异步操作,并注册该操作完成的处理程序。 异步操作完成后,执行系统将调用先前注册的处理程序,该处理程序将在调用“同步”操作时继续执行程序。


结果,半同步/半异步方法既包含异步方法的优点,也包含缺点。 本文的内容不允许我们详细考虑这种方法。 对于那些感兴趣的人,我建议您阅读《 POSA:并发和联网对象的模式 》一书中的同名章节。


半同步/半异步方法本身引入了一个新的“绿色流”实体-在程序或库执行系统级别的轻量级控制流。 使用绿色线程是程序员的选择。 它可以使用绿色线程池,可以为每个新请求创建一个新的绿色线程。 与OS线程/进程相比,区别在于绿色线程便宜得多:绿色线程消耗更少的RAM,并且创建速度更快。 这使您可以创建大量的绿色线程,例如,数十万个Go语言。 如此巨大的数量证明了使用绿色按需流动方法是合理的。


优点:


  • 它可以很好地扩展rps和同时连接的数量
  • 与异步方法相比,代码更易于编写和调试

缺点:


  • 由于操作的执行实际上是异步的,因此当单个同步操作阻塞整个过程时,可能会发生编程错误。 在通过库(例如Python)实现此方法的语言中,尤其如此。
  • 程序的不透明性。 使用线程或OS进程时,程序执行算法很明确:每个线程/进程都按照它们在代码中写入的顺序执行操作。 使用半同步/半异步方法,在代码中顺序写入的操作可能与处理并发请求的操作发生不可预测的交替。
  • 不适合实时系统。 请求的异步处理极大地增加了为每个单个请求的处理时间提供保证的复杂性。 这是上一段的结果。

根据实现的不同,此方法可以在CPU内核之间很好地扩展(Golang),或者根本不能扩展(Python)。
这种方法以及异步方法都允许您处理大量同时连接。 但是使用这种方法对服务进行编程比较容易,因为 代码以同步样式编写。


输送机加工


顾名思义,在这种方法中,请求由管道处理。 处理过程由多个OS线程按链状排列组成。 每个线程都是链中的一个链接;它执行处理请求所需的操作的某些子集。 每个请求顺序地通过链中的所有链接,并且每个时间的不同链接处理不同的请求。


优点:


  • 这种方法可以很好地扩展rps。 链中的链接越多,每秒处理的请求越多。
  • 使用多个线程可以使您跨CPU内核很好地扩展。

缺点:


  • 并非所有查询类别都适用于此方法。 例如,使用这种方法组织长时间的轮询将是困难且不便的。
  • 实现和调试的复杂性。 击败顺序处理,以便提高生产率可能很困难。 调试程序(其中每个请求在多个并行线程中按顺序处理)比顺序处理要困难得多。

范例:


  1. 高负载报告2018中描述了输送机处理的一个有趣示例。 莫斯科交易所交易和清算系统架构的演变

流水线已被广泛使用,但是大多数情况下,链接是独立进程(例如,通过消息队列或数据库)中交换消息的各个组件。


总结


所考虑方法的简要概述:


  • 同步处理。
    一种简单的方法,但是在rps和同时连接数方面的可伸缩性都非常有限。 它不允许同时使用多个CPU内核。
  • 针对每个请求的新流程。
    . , . . ( , ).
  • .
    , , . , .
  • /.
    /. . rps . . .
  • - (reactor ).
    rps . - , . CPU
  • Half sync/half async.
    rps . CPU (Golang) (Python). , () . reactor , , reactor .
  • .
    , . (, long polling ).

, .


: ? , ?


参考文献


  1. :
  2. - :
  3. :
  4. Half sync/half async:
  5. :
  6. :

Source: https://habr.com/ru/post/zh-CN432630/


All Articles