1.2.2 SQLOS
很多有SQL Server经验的读者可能会问,使用了这么久SQL Server,怎么从来没听说它还有OS?实际上,在使用SQL Server的过程中,我们早已接触到OS,只是没有察觉。比如有一套常用的系统视图,叫作sys.dm_os_xxxx,这里的sys即System Schema,dm即Dynamic Management(动态管理),os即SQLOS。
现在我们对照图1-14,看一下dm_os_threads、dm_os_workers等视图。这些视图其实都是在反馈SQLOS的内在情况。
当然也有非常难以解决的SQLOS问题,比如SOS_WAITING,这里的SOS也是SQLOS的缩写。
SQLOS存在的主要目的就是为了管理底层OS的资源,其中最重要的两个资源是CPU和内存。SQLOS从SQL Server 2005版本开始被引入,作为一个抽象层,后来随着版本的迭代,这个OS的功能被不断扩充,添加了很多新的资源调用关系,比如Extended Event等。
为什么要在Windows操作系统上再搭建一个“OS”来运行SQL Server呢?让我们一起来看看在引入SQLOS之前,SQL Server是如何请求CPU资源的。
早在SQL Server 7.0时,当时的SQL Server Scheduler完全依赖Windows Scheduler。CPU调度存在一个巨大的问题,就是操作系统经常会使用抢占式调度器(Preemptive Scheduler)。操作系统对SQL Server的线程和对其他任何程序的线程都一视同仁,所以会导致有高并发要求的SQL Server线程经常被打断,从而导致预期外的中断和上下文切换。
SQLOS Scheduler最大的不同在于它被设计成协作式调度器(Cooperative Scheduler)。举例来说,假如有一个SQL Server线程正在运行,但操作系统需要执行一个更紧急的Task,所以中断了SQL Server线程,而现在有了SQLOS Scheduler,它会把这个线程放到一个等待队列中,4ms之后,再重新申请。这就是线程自愿放弃CPU,等到Scheduler有空了,会重新把它从等待队列中取回来,这相对于抢占式就温和多了。
SQLOS有几个基本概念,比如在CPU方面,把CPU Processor(包括Hyperthread和物理的Core)抽象成Scheduler,而Scheduler采用yield的方式访问线程,线程被抽象成Worker。所以在SQL Server内部,使用Scheduler和Worker来描述实际的CPU调度。每个Worker的调度单位都是Request,但并不代表一对一的关系。一个Request又会被拆分成多个Task,然后由具体的Worker来执行,可以参考表1-1。
表1-1 SQLOS的基本概念
Scheduler的奥秘在于它独特的等待队列,因为普通CPU是不知道一条SQL语句下一步要等待什么的,并不能有效建立队列,开源数据库往往使用Mutex等方式尝试访问CPU,用以判断是否忙碌(后文再展开介绍),而有了Scheduler,就能有效地通过串行来完成并发任务的组织。
SQL Server队列的三种状态切换如图1-15所示。
图1-15 SQL Server队列的三种状态切换
SQL Server队列的三种状态说明如表1-2所示。
表1-2 SQL Server队列的三种状态说明
举例来说,SPID 100进程正在运行占用CPU,这时候它需要I/O操作,出现了新的Waiting Event——IO_COMPLETION,Scheduler会把它放到等待队列中,状态标记为Suspended。
准运行队列遵循FIFO原则,SPID 101进程会被推到Scheduler上运行占用CPU。
因为等待队列是非排序队列,这时候SPID 110进程原本等待的LCK已经拿到了,那么SPID 110进程也会被推到准运行队列中,但排在该队列的最后。
讲完了SQLOS的CPU调度原理后,我们来简单对比一下MySQL和SQL Server。
在MySQL中,如果一条update语句在processlist command状态下显示为updating,那么是否真的在更新呢?我们并不知道,有可能它已经在等待锁了,并没有在运行占用CPU,所以比较难判断它到底是否在消耗CPU,只有通过其他视图,或者抓取堆栈(Callstack)才能看到实际的情况。
而在SQL Server中,如果一条语句真的被标记为阻塞(Suspended),那么就意味着这个线程在等待队列,完全不占用CPU。如果此时实例的CPU开销很高,则只需要关注Running和Runnable的SPID,尤其是Running的会话,它们才是真正消耗CPU的会话。
下面对CPU调度简单做一个总结。
每一个SQL Server Session至少对应一个Worker,以此来访问CPU。
一个CPU Core/Scheduler在1s的时间内,可以提供1s的CPU时间。
CPU Core按时间片调度任务,假设一个Worker的某个Task在Scheduler上占据了完整1s的时间,在这自然时间的1s里,这个CPU Core的CPU TIME就是100%。
SQL Server使用SQLOS中的Scheduler和Worker,通过三种状态队列,即运行中队列(Running Queue)、可运行队列(Runnable Queue)和等待队列(Waiting Queue)的轮巡方式,实现高效地处理多任务请求。
这样的CPU管理比MySQL要高明得多,从本质上说,SQL Server使用了带权重的队列,CPU的上下文切换变得非常准确。而MySQL在高并发下很难准确切换线程,带来的结果就是并发度越高,CPU空转的可能性越大。
内存管理也是一个非常大的话题,有着诸多挑战。下面我们先来了解Windows的内存分配方式。
从图1-16中可以看到,应用程序在请求内存时,是无法知道自己的数据是存在于RAM中还是存在于虚拟内存中的,也无法保证自己的数据一定在RAM中。整个调度过程完全由Windows Memory Manager决定。
图1-16 Windows VAD与PAD
这里有一个非常重要的概念必须要提一下,它就是NUMA(Non-Uniform Memory Access),如图1-17所示。
NUMA node with 4 processors
图1-17 NUMA示意图(引用自微软官方文档)
在讨论NUMA之前,我们还是先了解一下历史——在NUMA出现之前,OS的CPU和内存是如何工作的。
假设有一个4核的CPU和一个4GB的内存条,4核的CPU在访问这个内存条时,走的是一根总线(BUS)。如果是64核的CPU,还是访问这个内存条,走的还是一根总线的话,我们就会发现,总线成为SMP(Symmetric Multiprocessing)系统的瓶颈。
于是,NUMA出现了。
Physical NUMA是我们常说的NUMA方式。即图1-17所展示的,多个CPU Core和一组Memory组成一个Node,这个Node有自己的系统总线,甚至有的物理硬件还会支持自己的I/O通道。
CPU Core所在Node的Memory称为Local Memory,“隔壁”的Node的Memory称为Foreign Memory或者Remote Memory。如图1-18所示,Local Memory的性能显然优于Remote Memory。
图1-18 Local Memory和Remote Memory
Logical NUMA即软件层面实现的NUMA,就不在本例中展开讨论了。
想要确认自己的Windows是否运行在NUMA结构下,可以在任务管理器中轻松地查看是否是NUMA结构,只有NUMA结构才可以展示NUMA节点负载,如图1-19所示。一般笔记本电脑、PC的硬件都不支持NUMA,而服务器支持NUMA。
图1-19 任务管理器中的NUMA选项
所以说并不是所有应用程序都能有效支持NUMA,或者说有效使用Local Memory。而SQL Server在这一点上做到了自动适配,在SQL Server的Errorlog中,我们会看到关于NUMA Mask的适配情况。
怎么看这个Mask呢?这里的f是十六进制的,转换成二进制的,就是:
1111 1111 1111 1111
这就表示16个Core Socket,进而表示16个插槽都有Core,所以是一个16核的Node。
在SQL Server 2005至2008R2版本中,single-page和multi-page是分开使用的,其中single-page全部来源于缓冲池(Buffer Pool),所以受控于“max server memory”参数;而multi-page,即请求大于8KB的页,是从外部获取的。这也是使用2008R2以前的版本可能会遇到内存泄漏(Memory Leak)或内存溢出(Out of Memory,OOM)的一些场景的原因之一,SQL Server对这部分内存缺少管控力。请参考图1-20。
图1-20 SQL Server 2012以前的内存管理示意图
SQL Server经过多个版本的迭代,从SQL Server 2012开始,正式将single-page和multi-page的使用合并到统一的内存管理中,但CLR type依然游离在内存管理之外,因为CLR使用的是Hosting API,并不在内存管理范围内。请参考图1-21。
图1-21 SQL Server 2012以后的内存管理示意图
在SQL Server中,这些内存是如何分配和追踪的呢?我们需要拆解内存管理。SQL Server一共有4个层级的组件来实现分配和追踪内存,它们分别是Memory Broker、Memory Clerk、Memory Node(即NUMA Node)和Memory Pool(Resource Governor内的)。
Memory Broker用来监听和响应各种内存请求的事件,比如在一条SQL语句的生命周期中,需要消耗内存的地方有:
Optimizing:解析和优化执行计划时。
Execution:执行具体的SQL语句时。
Data:热数据处理时。
Memory Broker的意义在于打通不同类型的内存分配和调度。Memory Clerk则根据不同的内存类型加以分类管理和追踪,属于接口层。Memory Clerk有非常多的Clerk type,读者可以在sys.dm_os_memory_clerks中探索和发现更多的Clerk,如图1-22所示。
图1-22 sys.dm_os_memory_clerks相关信息
从图1-22中也可以看出,reserved memory size完全不等于committed size,commit的大小才是真实的内存开销。
而且从SQL Server 2012开始,企业版引入了Resource Governor的概念,即使不配置Governor,在默认情况下,SQL Server也会设置两个Pool,其中一个是Internal Pool;另一个是Default Pool。
Internal Pool主要用来存放SQL Server自己的Backend系统线程的资源;Default Pool主要用来存放默认的用户线程的资源。
现在我们重新看一下SQL Server的内存分配,如图1-23所示。
图1-23 内存管理过程
现在的SQL Server已经很少遇到内存泄漏的场景了,在内存管理上,对比MySQL的开源malloc的使用,SQL Server确实做得比较成熟。