系统配置
aio-max-nr
1 | sudo vim /etc/sysctl.conf |
perf_event_open
1 | sudo vim /etc/sysctl.conf |
命令行配置
1 | debug |
1 | sudo vim /etc/sysctl.conf |
1 | sudo vim /etc/sysctl.conf |
1 | debug |
1 | seastar::app_template app; |
std::move
1 | seastar::future<int> slow_do_something(std::unique_ptr<T> obj) { |
mutable
:
The
[obj = ...]
capture syntax we used here is new to C++14. This is the main reason why Seastar requires C++14, and does not support older C++11 compilers.The extra
() mutable
syntax was needed here because by default when C++ captures a value (in this case, the value of std::move(obj)) into a lambda, it makes this value read-only, so our lambda cannot, in this example, move it again. Addingmutable
removes this artificial restriction.
1 |
|
抛出异常会导致整个 f()
执行失败,不建议抛出异常,应该返回fail future:
1 | void inner() { |
或者使用futurize_apply
:
1 | seastar::future<> fail() { |
seastar 入门, 基于这里 这里.
seastar 使用两个概念 futures 和 continuations,提供复杂的异步编程库,使用非共享编程模型。
大多数程序处理并行的请求,通常是每个连接使用隔离的操作系统级别的进程。这种技术也一直在进化,从一开始的每个请求都会创建一个进程,到创建一个线程池,最终进化到使用线程。不过,进化的共同点是在一个时刻,每个进程/线程处理一个连接。
异步/时间驱动的服务,通常一个核心对应一个线程。
seastar 是一个事件驱动的框架,允许你使用相对直接的方式编写非阻塞、异步代码。他的 API 基于 future,利用下面的概念达到极致性能。
每个 seastar 应用必须定义和运行一个 app_template 对象,该对象启动主事件循环(seastar 引擎)在一个或多个 cpu 核心上,运行指定的函数。
方法返回一个 future,指示何时退出程序,如果是返回 make_ready_future<>
则会立刻退出程序。无论何时,C 的 exit()
方法都不应使用,这会阻止 seastar 或程序进行适当的清理工作。
seastar 应用在每个核心上运行一个线程,每个线程运行自己的 event loop,可以使用 seastar::smp::count
打印运行的线程数。
Seastar是支持高并发和低延迟的高性能异步编程库,用于在现代多核机器上编写高效复杂的服务器应用程序。
由于现代多核和多插槽机器在核心之间共享数据(原子指令,缓存线弹跳和内存防护)有严重的惩罚。Seastar程序使用无共享编程模型,即可用内存在核心之间划分每个核心在其自身的内存部分中处理数据,核心之间的通信通过显式消息传递发生(当然,这本身就是使用SMP的共享内存硬件发生的)。
对于并发请求,传统的网络服务器处理改善流程:
每个进程在同一时刻,只处理一个连接。哪怕一个进程阻塞,还会有其他进程来处理其他请求。
对每个连接使用进程(或线程)的服务器进行编程称为同步编程,因为代码是线性编写的,并且一行代码在前一行完成后开始运行。
尽管同步服务器应用程序是以线性,非并行方式编写的,但在后台,内核有助于确保所有内容并行发生,并且机器的资源(CPU,磁盘和网络)得到充分利用。除了进程并行性(我们有多个进程并行处理多个连接)之外,内核甚至可以并行化一个连接的工作 - 例如,处理未完成的磁盘请求(例如,从磁盘文件中读取)与处理并行网络连接(发送缓冲但尚未发送的数据,并缓冲新接收的数据,直到应用程序准备好读取它)。
启动一个新进程、上下文切换很慢,并且每个进程都带来了很大的开销 - 最明显的是它的堆栈大小。服务器和内核开发者努力减轻这些开销:从进程切换到线程,从创建新线程到线程池,他们降低了每个线程的默认堆栈大小,并增加了虚拟内存大小以允许更多部分利用的堆栈。但是,具有同步设计的服务器的性能并不令人满意,并且随着并发连接数量的增长而严重缩放。
编写异步服务器程序面临两大挑战:
此外,当需要最佳性能时,服务器应用程序及其编程框架别无选择,只能考虑以下因素:
Seastar是一个用于编写异步服务器应用程序的框架,旨在解决上述所有四个挑战:
现代硬件工作负载的运行方式与当前编程范例所依赖的硬件明显不同,并且设计了当前的软件基础架构。
核心数增加 时钟速度保持稳定
各个内核的时钟速度性能提升已经停止。核心数量的增加意味着性能取决于跨多个核心的协调,而不再取决于单个核心的吞吐量。在新硬件上,标准工作负载的性能更多地取决于跨核心的锁定和协调,而不是单个核心的性能。软件架构师面临两个没有吸引力的选择:
同时 I/O继续提高速度
现代系统上可用的网络和存储设备的速度也在不断提高。但是,CPU核心处理任何一个核心上的数据包的能力都没有。
无共享模型
并发编程难在共享数据,正是因为共享数据的存在,我们的才需要用各种手段保护它们,才需要在多个核之间同步数据。Seastar会在每个核上创建一个线程,并将此线程绑定在其上。不同核(线程)之间禁共享数据,只能通过消息队列来传递数据。
由于跨核心共享信息需要昂贵的锁定,因此Seastar使用无共享模型将所有请求分片到单个核心。
Seastar每个核心运行一个应用程序线程,并依赖于显式消息传递,而不是线程之间的共享内存。此设计避免了缓慢、不可扩展的锁定和缓存跳出。
必须明确处理跨核心的任何资源共享。例如,当两个请求是同一会话的一部分,并且两个CPU各自获得依赖于相同会话状态的请求时,一个CPU必须显式地将请求转发给另一个。CPU可以处理任一响应。Seastar提供限制跨核通信需求的设施,但是当通信不可避免时,它提供高性能的非阻塞通信原语,以确保性能不会降低。
每个core上运行的线程只处理自己所负责的数据,把异步事件通过Promises的模式调度起来。这样的设计达到了shared-nothing的效果,所以避免了多核之间的锁。
核心间通讯
Seastar为核心之间的通信提供了几个相关的功能。最简单的是:
1 | smp::submit_to(cpu, lambda) |
这是一个承诺。它返回一个future,它是lambda的返回值。它在指定的cpu上运行lambda并返回结果。
1 | smp::submit_to(neighbor, [key] { |
线程环境中的等效项需要锁定数据库对象。锁定操作本质上是昂贵的,并且还可以在旋转中强制上下文切换或浪费CPU周期,这取决于所使用的锁定方案。
跨核通信的其他变体允许向所有CPU广播值,或者向所有CPU发送lambda的map / reduce操作,收集结果,并应用转换以减少到单个值。
Seastar在两个平台上支持四种不同的网络模式,所有这些都没有更改应用程序代码
可以将同一应用程序构建为专用服务器设备或基于内核的VM。
可供选择的网络
Linux中常见的网络功能非常全面,成熟且高性能。但是,对于真正的网络密集型应用程序,Linux堆栈受到限制:
通过使用Seastar基本原语实现的用户空间TCP / IP堆栈,可以避免这些约束。Seastar本地网络享有零拷贝、零锁定和零上下文切换性能。
另一种用户空间网络工具包DPDK专为快速数据包处理而设计,通常每个数据包的CPU周期少于80个。它与Linux无缝集成,以利用高性能硬件。
Seastar专为面向未来的开发而设计:可以构建和运行相同的应用程序,以便在部署时最佳工作的网络模式下运行,而不必提前提交经济上不可预测的技术选择。
并行化的范例
软件开发的挑战
“必须编写程序供人们阅读,并且只能偶然让机器执行。 “硬件已经改变到最初对少量CPU内核进行的假设不再有效的程度。
进程非常独立,但开销很高
线程会给程序员和应用程序基础架构带来额外的协调成本,并且难以调试
纯事件驱动编程可能导致难以测试和扩展的代码库
一个理想的解决方案:
解决方案:Seastar futures and promises
这种解决模型被称为“futures and promises”
Future是一种数据结构,代表一些尚未确定的结果
Promise是Future的提供者
将promise / future对视为先进先出队列有时会有所帮助,其最大长度为一个项目,只能使用一次。Promise是队列的产生结束,而Future是消费结束。像FIFO一样,Future和Promise用于解耦数据生产者和数据消费者。
基本Future和Promise在C ++标准库和Boost中实现
然而,Seastar对Future和Promise的优化实现是不同的。虽然标准实现针对的是粗粒度任务,可能会阻塞并需要很长时间才能完成,但Seastar的Future和Promise用于管理细粒度、无阻塞的任务。为了有效地满足这一要求:
示例
1 |
|
说到shared-nothing,没有跨核通讯是不太可能的,对于跨核的通讯,框架中使用了无锁队列来实现。
线程应用程序需要固有且昂贵的锁定操作,而Seastar模型可以完全避免跨CPU通信的锁定。
从程序员的角度来看,Seastar使用Future,Promise和Continuations(f / p / c)。使用epoll和诸如libevent之类的用户空间库的传统事件驱动编程使编写复杂应用程序变得非常困难,f / p / c使得编写复杂的异步代码变得更加容易。
例如,发送方核心C0和接收机核心C1之间的以下交互可以在不需要锁定的情况下进行。
每个实际队列,一个用于请求,一个用于满足请求的返回队列,是一个简单的指针队列。系统上每对CPU核心有一个请求队列和一个返回队列。由于核心不与自身配对,因此16核系统将具有240个请求队列和240个返回队列。
从程序员角度来看
Seastar提供了一组通用的编程结构来管理内核之间的通信。例如:
1 | return conn->read_exactly(4).then(temporary_buffer<char> buf) { |
调试
并行编程是开发人员技能,随着CPU数量和工作负载并行化的增加而越来越高。虽然有效利用CPU时间是必要的,但对于大多数项目来说,有效使用程序员时间更为重要。Seastar是唯一实现世界级性能和可理解、可测试的框架。
线程
基于Seastar的程序在每个CPU核心上运行一个线程,每一个线程都运行自己的事件循环,在Seastar命名法中称为引擎。默认情况下,Seastar应用程序将接管所有可用内核,每个内核启动一个线程。
内存
每个线程都预先分配了一大块内存(在它运行的同一个NUMA节点上),并且该线程仅使用该内存进行分配(例如malloc()或new)。
Futures 和 Continuations 用于构建异步编程块。它们的优势是:
Futures通常由异步函数返回。
Continuation是当Future变得可用时的回调函数;通过then()
附加到Future。
Future编程模型允许程序员封装复杂的异步操作,Seastar引擎负责在适当的时候运行Future的Continuation。
当Future就绪调用Continuation已经得到优化,不会在事件循环的下一次迭代中注册以便执行。then()
的实现包含计数器,不会无限制地执行,导致其他Continuation无法执行。
在Seastar中,Continuations是lambda表达式,传递给Future的then方法。lambda对于Seastar的异步编程来说,有一个非重要的特性:Lambdas可以捕获状态。
Lambdas 不仅仅是一个函数 ,它实际上是一个对象,包含代码和数据。从本质上讲,编译器会自动为我们创建状态对象,我们既不需要定义它,也不需要跟踪它(当Continuations需要延期执行时,它会与Continuations一起保存,并在继续运行后自动删除)。
当一个Continuations捕获状态并立即执行时,不会产生额外运行时开销。但是,让Continuations不能立即执行时(因为Future还没有就绪),需要在堆上为这些数据分配内存,并且需要在那里复制Continuations捕获到的数据。这会有运行时开销,但这是不可避免的,并且与线程编程模型中的相关开销相比小很多(在线程程序中,这种状态通常驻留在被阻塞线程的堆栈上,但堆栈要比Lambda捕获的状态大得多,占用大量内存并导致大量缓存,影响线程之间的上下文切换)。
C++中有两种捕获:
使用引用捕获通常会出错,因为如果很久才能执行Continuations,那么捕获的引用可能变为不相关的内容。但是可以使用do_with()
来解决这个问题,确保一个对象在Continuations的整个生命周期存在,使得引用捕获成为可能。
使用移动捕获也很有用,通过将对象移动到Continuations,将此对象的所有权转移到Continuations,使得Continuations结束时自动删除对象变得容易。
处理异常的原语:
.then_wrapped()
.finnaly()
异步函数执行一个操作,该操作可能在函数返回后很长时间内继续:函数本身几乎立即返回一个Future,但可能需要一段时间才能完成Future的计算。
当这样的异步操作需要对现有对象进行操作或使用临时对象时,我们需要担心这些对象的生命周期:确保在异步函数完成之前不会销毁这些对象(否则会尝试使用释放的对象,导致故障或崩溃),并确保对象最终在不再需要时被销毁(否则会内存泄漏)。
Seastar提供各种机制,可以在适当的时间内安全有效地保持对象的存活。
将所有权转移到Continuation。
在Continuation运行并且之后被销毁时,确保对象处于活动状态的最直接的方法是将其所有权传递给Continuation。当Continuation拥有该对象时,在运行时该对象将被保留,并且一旦不需要,就会被销毁。
一种更简单的方法是让异步函数的调用者继续成为对象的所有者,并将对象的引用传递给需要该对象的其他异步函数和Continuations。
将对象的副本捕获到一个Continuation里面虽然很简单,但是对于复杂的对象,复制通常代价很大,甚至有些对象无法复制。这些问题的解决方案就是引用计数。
把对象保存在堆栈。
Seastar借助以下概念,实现极致性能:
并行的标准方法是使用线程。然而,该模型具有许多性能缺陷,因此Seastar使用不同的模型。Seastar使用分片或分区来管理多个核心。每个核心都分配了机器上的连接和数据的子集 ,每个核心和其他核心一起对所有的连接和数据共享处理任务。如果核心上的计算需要访问驻留在另一个核心上的数据,则它必须显式向远程核心发送消息,要求它读取或写入数据,并等待结果。
线程和进程是操作系统提供的抽象。操作系统不是拥有一个或少量固定数量的处理器,而是允许用户根据自己的喜好创建尽可能多的虚拟处理器,并在物理处理器之上复用这些虚拟处理器。这些虚拟处理器称为线程(如果它们彼此共享内存)或进程(如果它们不共享)。
最简单的线程方法是每个连接线程。对于需要提供的每个连接,都会创建一个线程,在该线程中运行读取进程响应循环。这种方法存在许多问题,将其局限于最简单的应用程序:
因此,大多数线程应用程序现在都使用线程池。这里,大量连接被复用在较少数量的线程之上(它们本身在多个处理器的顶部被复用)。读取线程将等待连接变为活动状态,将其分配给线程池中的空闲线程,然后线程池将读取请求,处理请求并进行响应。
但是,这些线程设计仍存在性能问题:
分区化的网络连接
为了对连接进行分区,seastar会自动划分核心之间的连接。它利用现代网络接口卡(NIC)的能力,为每个核心提供数据包队列,并自动将数据包子集转移到这些队列。这样,每个Seastar核心接收所有连接的共享,并且属于那些连接的所有分组由该核心处理。
分区化的数据
Seastar无法自动分区数据。用户必须选择分区方法并将处理转移到正确的核心。一些分区策略包括:
一类应用程序特别适合分区 - 横向扩展服务器类。这些已经跨节点分区,因此核心之间的分区仅使用节点内部分片扩展模型。
对网络和数据进行分区化的好处:
基于分片的异步编程框架
对比传统数据栈
内存切片(shard)
网络分片
用户态task调度
用户态磁盘 I/O 的调度
Seastar利用操作系统libaio提供的io_submit去提交磁盘操作和io_getevent来收集操作结果,从而实现磁盘I/O操作的异步性。但Linux对于aio支持的并不是很好,并不是所有文件系统都支持aio,即使有的支持,也有很多问题。
最新的xfs对于aio支持的很好,所以对于disk IO,只推荐采用xfs。由于在内核中,从文件系统(file system)到块设备层(block level)再到具体的存储设备层,每个层都有I/O队列,对I/O进行了自己的管理。一旦storage I/O出现拥塞,不太容易判断哪层出现问题,也不好采取措施进行调控。
因此,Seastar在用户态实现了I/O scheduler,对磁盘I/O进行精确的分级控制和调优。Seastar有自己的I/O queue来缓存I/O,并实现了各种I/O priority class,从而保证各种I/O调度的公平性。下图示意了Seastar实现的用户态I/O调度器:
用户态原生网络栈(Native network stack)
下图描绘了Seastar的网络栈,即包含Posix stack,也包含原生网络栈:
除了支持常规的Posix network stack,Seastar还支持基于DPDK的native network stack。大家知道,DPDK是Intel推出的一个高性能用户态网络管理包,其核心思想和Seastar是一致的:用户态轮询(poll)模式的网卡驱动,没有中断,没有上下文切换,没有内存拷贝,无锁,share-nothing,自己的内存管理,利用NUMA等等。
DPDK主要提供L2数据包处理功能,需要上层应用提供L3及以上的网络管理。Seastar实现了TCP/IP协议:每个core绑定到物理网卡的一个接受队列和发送队列,这样所有的数据连接也被分片(shard),每个core自始至终只负责自己那部分数据连接。对于native network stack,没有syscall调用,没有多余的数据复制,没有锁,性能当然最好。
Seastar是一个应用框架,它几乎将操作系统所提供的抽象完整地搬移到了用户态中,以减少操作系统的抽象开销,实现软硬件一体化。
可以将Seastar想象成一个支持多核的操作系统,每个核上运行着许多的执行流。但是与操作系统不同的是,这些执行流有固定的宿主核,每个执行流从头到尾只能在一个核上运行,并且,位于不同核上的执行流之间,只能通过跨核消息来通信。
Seastar将每个核抽象成一台单核计算机,每个单核计算机上运行着许多执行流,一个单核计算机上的多个执行流可以共享数据,不同单核计算机上的执行流只能通过消息来共享数据。
现在考虑Seastar如何提供执行流抽象的。执行流的本质是一系列微任务被链接起来后形成的一个微任务链。为什么是微任务链,而不是一个连续的宏任务?因为,我们的任务通常涉及到IO,而IO并不总是可用的,比如,读一个socket时,其内还没有数据。此时,我们显然需要让出CPU,等待socket可读,才进行接下来的步骤。于是,我们的一个完整的宏任务被拆成了很多微任务,这些微任务被链接起来后,即是我们的宏任务,也即是我们的执行流。
当然,复杂的微任务甚至可以构造一个有向无环图。这个有向无环图会由Seastar抽象的用户态CPU按照拓扑序来调度执行。
那么,Seastar是如何将这些微任务链接起来的?一种简单的方法是回调函数。一种复杂的方法是提供用户态线程的抽象,即所谓协程,给用户提供一个单协程占用整个CPU的抽象。Seastar提供了另一种抽象,FPC,即future-promise-continuation。FPC使得构造有向无环图更加方便,使用协程的话,还需要提供latch/conditional variable等同步原语,才能构造更加复杂的执行流。
Seastar在每个用户态CPU上运行一个调度器,来调度一系列的微任务。
Share-nothing的用户态执行流的抽象降低了切换开销以及同步开销,然而,同一进程内,内存是共享的,分配与释放内存时,依然会有同步的存在。为了避免此问题,Seastar在应用启动时,将整个虚拟地址空间按照CPU核数等分为若干块,每个CPU使用自己的内存块进行内存分配与释放,从而避免同步。
Seastar是一个异步框架,任何一个核阻塞都会造成核上的待调度的微任务严重超时。然而,令人无奈的是,传统文件系统操作是同步阻塞的。好在AIO的存在解决了这一问题(虽然现在AIO还是一堆坑)。AIO有一些固有的限制,它必须以O_DIRECT方式打开文件,导致不能使用pagecache以及读写必须对齐。为了解决AIO的问题,Seastar维护用户态PageCache,从而实现了Zero copy的文件操作。并且,它维护自己的IO调度策略,从而更好地使用磁盘。
Seastar支持多种形式的网络操作,一是传统的epoll方式,这种方式已经非常成熟,并且在业内有广泛应用。另一种是用户态网络栈+DPDK,从而实现Zero copy与Zero switch的网络操作,进一步提高了网络的性能。
Seastar当前专注于高吞吐量,低延迟 I/O 应用:
计算机科学中,future、promis、delay、deferred,是指用于在一些并发编程语言中同步程序执行的方式。他们描述了一个对象,该对象充当最初未知的结果的代理,通常是因为其值的计算尚未完成。
通常,future、promis、delay、deferred 可以互换使用。future是变量的只读占位符,而promise是可写的单个赋值容器,用于设置future的值。
可以定义 future 而不指定特定的 promise 设置其值。future 和 promise 相互关联:future 是值,promise 是设置值得函数。
future 和 promise 起源于函数式编程,将值(a future)与其计算方式(a promise)分离,从而允许更灵活地进行计算,特别是通过并行计算。
常见 Future 比如有:
Promise 和 Future 简化了异步编程,因为它们将事件生成器(promise)和事件使用者(使用future的任何人)分离。 不管promise是否在future使用前完成,或者相反,都不会改变代码的输出结果。
消费Future
使用then()
方法消费一个Future,并提供一个回调函数。
1 | future<int> get(); // promises an int will be produced eventually |
链式Future
then()
可以返回一个future。
1 | future<int> get(); // promises an int will be produced eventually |
Loops
通过尾调用实现循环
1 | future<int> get(); // promises an int will be produced eventually |
异常捕获
如果一个then()
抛出异常,调度器会捕获异常,并取消所有依赖的then()
的调用。如果想捕获异常,可以在语句最后添加.then_wrapped()
。
1 | future<buffer> receive(); |
数据平面开发套件(DPD,Data Plane Development Kit),主要基于Linux系统运行,用于快速数据包处理的函数库与驱动集合,可以极大提高数据处理性能和吞吐量,提高数据平面应用程序的工作效率。
DPDK架构通过创建EAL(Environment Abstraction Layer,环境抽象层)来为不同的工作环境创造函数库集,创建后开发者即可把自己的应用与函数库进行链接。
工作原理
DPDK使用了轮询(polling)而不是中断来处理数据包。在收到数据包时,经DPDK重载的网卡驱动不会通过中断通知CPU,而是直接将数据包存入内存,交付应用层软件通过DPDK提供的接口来直接处理,这样节省了大量的CPU中断时间和内存拷贝时间。
环境抽象层
DPDK的创造的环境抽象层(EAL, Environment Abstraction Layer)主要负责对计算机底层资源(如硬件和内存空间)的访问,并对提供给用户的接口实施了实现细节的封装。其初始化例程决定了如何分配这些资源(PCI设备、计时器、控制台等)。
轮询模式驱动
DPDK包括1Gb,10Gb,40Gb和半虚拟化抽象层的轮询模式驱动(PMD, Poll Mode Driver)。PMD由用户空间的特定的驱动程序提供的API组成,用于对设备和它们相应的队列进行设置。抛弃了基于中断的异步信号发送机制为该架构带来很大的开销节省。避免中断性能瓶颈是DPDK提升数据包处理速度的关键之一。
DPDK环境为数据包处理应用考虑了两种模型:运行至完成(run-to-completion)模型和管道(pipeline)模型。
BEFORE
AFTER
技术
用户态驱动实现zero-copy。如果使用linux内核的协议栈发包,会多一次用户态到内核态的内存数据copy。linux内核已经支持用户态驱动的framework,编写用户态驱动可以减少内存copy
传统的linux网卡驱动是基于cpu中断方式,内核发送线程首先将数据copy到发送缓冲区后自己进入睡眠,网卡驱动程序将缓冲区数据发送完毕后,会给cpu一个中断信号,cpu在中断处理程序中将发送线程从睡眠中唤醒。这就导致了大量的cpu中断,特别虚机的场景通过软中断方式进行模拟,高并发的场景会导致cpu利用率很高。DPDK的做法是,在缓冲区头部有一个标记位,记录该缓冲区数据发送的状态,驱动程序发送完数据后,将缓冲区的标记为置为空闲,然后有一个轮询线程不断去检测该标记位,检测发送成功后就会回收内存并做相应的回调处理。这样就减少了CPU的中断次数
高速发包需要大量的申请和回收内存缓冲区,需要一个高效的内存池管理,算法和数据结构有很多,需要根据实际的应用场景择优选择
ring buffer,环形队列。发送者和接收者的需要一个无锁的结构提高并发的效率。简单的环形队列支持一个读者一个写着,多个生产者和消费者的工作队列学术界和工业界也有现成的成果和实现
DPDK并没有实现一个完整的TCP/UDP/IP协议栈。
使用异步编程的原因
NUMA是一种CPU架构。
NUMA(Non Uniform Memory Access Architecture, 非统一内存访问)技术可以使众多服务器像单一系统那样运转,同时保留小系统便于编程和管理的优点。基于电子商务应用对内存访问提出的更高的要求,NUMA也向复杂的结构设计提出了挑战。
NUMA架构在逻辑上遵循对称多处理(SMP)架构。
限制访问存储器的次数是现代计算机提高性能的要点。 NUMA通过提供分离的存储器给各个处理器,避免当多个处理器访问同一个存储器产生的性能损失来试图解决这个问题。对于涉及到分散的数据的应用(在服务器和类似于服务器的应用中很常见),NUMA可以通过一个共享的存储器提高性能至n倍,而n大约是处理器(或者分离的存储器)的个数。
关于状态机的一个极度确切的描述是它是一个有向图形,由一组节点和一组相应的转移函数组成。状态机通过响应一系列事件而“运行”。每个事件都在属于“当前” 节点的转移函数的控制范围内,其中函数的范围是节点的一个子集。函数返回“下一个”(也许是同一个)节点。这些节点中至少有一个必须是终态。当到达终态, 状态机停止。
SMP的全称是 对称多处理(Symmetrical Multi-Processing)技术,是指在一个计算机上汇集了一组处理器(多CPU),各CPU之间共享内存子系统以及总线结构。
它是相对非对称多处理技术而言的、应用十分广泛的并行技术。在这种架构中,一台电脑不再由单个CPU组成,而同时由多个处理器运行操作系统的单一复本,并共享内存和一台计算机的其他资源。虽然同时使用多个CPU,但是从管理的角度来看,它们的表现就像一台单机一样。系统将任务队列对称地分布于多个CPU之上,从而极大地提高了整个系统的数据处理能力。所有的处理器都可以平等地访问内存、I/O和外部中断。在对称多处理系统中,系统资源被系统中所有CPU共享,工作负载能够均匀地分配到所有可用处理器之上。
进程
程序
线程
对比
DMA(Direct Memory Access,直接内存存取 / 直接存储器访问) 是所有现代电脑的重要特色,它允许不同速度的硬件装置来沟通,而不需要依赖于 CPU 的大量中断负载。否则,CPU 需要从来源把每一片段的资料复制到暂存器,然后把它们再次写回到新的地方。在这个时间中,CPU 对于其他的工作来说就无法使用。
在处理web请求时,通常有两种体系结构,分别为:thread-based architecture(基于线程)、event-driven architecture(事件驱动)。
基于线程
基于线程的体系结构通常会使用多线程来处理客户端的请求,每当接收到一个请求,便开启一个独立的线程来处理。这种方式虽然是直观的,但是仅适用于并发访问量不大的场景,因为线程需要占用一定的内存资源,且操作系统在线程之间的切换也需要一定的开销,当线程数过多时显然会降低web服务器的性能。并且,当线程在处理I/O操作,在等待输入的这段时间线程处于空闲的状态,同样也会造成cpu资源的浪费。一个典型的设计如下:
事件驱动
事件驱动体系结构是目前比较广泛使用的一种。这种方式会定义一系列的事件处理器来响应事件的发生,并且将服务端接受连接与对事件的处理分离。其中,事件是一种状态的改变。比如,tcp中socket的new incoming connection、ready for read、ready for write。
Reactor
reactor设计模式是event-driven architecture的一种实现方式,处理多个客户端并发的向服务端请求服务的场景。每种服务在服务端可能由多个方法组成。reactor会解耦并发请求的服务并分发给对应的事件处理器来处理。目前,许多流行的开源框架都用到了reactor模式,如:netty、node.js等,包括java的nio。
Reactor是事件驱动机制,和普通函数调用的不同之处在于:应用程序不是主动调用某个API完成处理,Reactor逆置了事件处理流程,应用程序需要提供相应的接口并注册到Reactor上。如果相应的事件发生,Reactor将主动调用应用程序注册接口(PS:又将这些接口称为回调函数)。
Reactor应用于编写高性能网络服务器技术之一,优点如下:
两种实现方式:
在Reactor中,这些被拆分的小线程或者子过程对应的是handler,每一种handler会出处理一种event。这里会有一个全局的管理者selector,我们需要把channel注册感兴趣的事件,那么这个selector就会不断在channel上检测是否有该类型的事件发生,如果没有,那么主线程就会被阻塞,否则就会调用相应的事件处理函数即handler来处理。
Reactor模式首先是事件驱动的,有一个或多个并发输入源,有一个Service Handler,有多个Request Handlers;这个Service Handler会同步的将输入的请求(Event)多路复用的分发给相应的Request Handler。
Reactor模式则并没有Queue来做缓冲,每当一个Event输入到Service Handler之后,该Service Handler会主动轮询,根据不同的Event类型将其分发给对应的Request Handler来处理。
Reactor模式结构
处理流程
占用大量CPU的操作:
可以分为三类:
对于前两类问题,其根本来源都是内核抽象的开销,想要优化掉它,我们必然减少对内核的依赖,即,将内核提供的抽象,上移到应用层中,在应用层中实现这些抽象。典型的例子如:
对于并发开销,其优化思路则非常多:
这些名词外在体现千差万别,但其实核心都在于,Zero switch,避免上下文切换。上下文切换的影响包括但不限于Flush TLB、Flush L1、等待重新调度、CPU消耗。无论是Cache miss还是等待重新调用,都在很大程度上提升代码的延迟。
使用mutex时,当加锁失败,线程会被切换出去(阻塞)。
实现方式:
1 | opts |
1 | !/bin/bash |
1 | !/bin/bash |
1 | brew |
1 | pyenv install -l |
1 | pyenv install <version> # e.g 3.8.9 |
1 | pyenv global 3.8.9 |
1 | pyenv versions |
1 | pyenv global system |
1 | consul agent -dev -bind=0.0.0.0 -client=0.0.0.0 -advertise=127.0.0.1 |
1 | curl -XGET http://localhost:8500/v1/catalog/services |
1 | curl -XGET http://localhost:8500/v1/health/service/:servicename |
1 | curl -XGET http://localhost:8500/v1/catalog/nodes |
1 | curl -XPUT http://localhost:8500/v1/agent/force-leave/:node_name |
1 | consul kv export > data.json # 导出所有 |
1 | consul kv import @data.json # 文件导入, 需要在文件名前加 @ |
1 | 列出所有 key |
注意
指定参数 index={latest-index}
,latest-index 是相对于 kv prefix 而言,在 response 的 header 中 X-Consul-Index
返回。
1 | function is_enable() { |
使用界面删除卷时,一致失败,查看 cinder-volumn 日志,错误是卷在使用。
1 | ... |
1 | root@srv:/home/wii# blkid |
可以看到,是 tgt 服务一直在占用。
1 | root@srv: service stop tgt |
再删除卷,然后启动 tgt 服务。
1 | root@srv: service start tgt |
如果存储服务区分 ssh 和 hdd,又希望系统盘能用 ssd,可以修改卷的类型扩展参数。
LVM_NVME 要在 cinder 的配置进行配置,对应的是 ssd 磁盘。