设计高性能的应用程序 1/3 (Windows, 多线程 + IOCP)

原文链接: http://blogs.technet.com/b/winserverperformance/archive/2008/06/26/designing-applications-for-high-performance-part-1.aspx

 

 

Designing Applications for High Performance

25 Apr 2008 12:40 AM 

Rick Vicik - Architect, Windows Server Performance Team

 

 

设计高性能的应用程序 1/3

25 Apr 2008 12:40 AM 

Rick Vicik - 架构师,Windows Server 高性能组

 

最近几年开始,处理器的性能每年提高的不如以前快了,所以程序员们必须学会怎么设计软件才能在多核处理器上发挥更高的性能,扩展性。

我(作者)在最近20年的时间里,在SQL Server开发和Windows Server性能小组里主要专注于关于多核性能和扩展性的一些问题。在这些年中我遇到过许多经常发生,但开发者们应该尽量避免情况。

 

在这个分成三个部分的系列中,我会描述这些低效率的情况,并且提供一些可以提高软件的扩展性和效率的一些建议。

本文主要面对的是服务端的程序,不过基本原理也适用于所有其他类型的应用程序。

 

目前高性能计算的问题的根本原因在于处理器的速度比RAM的速度要快得多,所以我们需要类似于缓存技术,

否则处理器的大部分处理能力都要浪费在访问内存操作上了。访问任何类型的缓存的效率主要取决于数据的locality。

就算是在单核处理器中,比较差的locality会造成指数级的降低系统的性能。而多核处理器中的性能会下降的幅度将会更大,因为在多核处理器中,缓存中的数据会被经常复制于多个缓存之间以满足数据一致性的要求。(执行一次数据同步的操作非常复杂)

并且当程序产生一些需要在所有处理器之间共享的数据的时候,这些数据会占满处理器之间的网络带宽(比如总线),然后会降低其他所有处理器内存访问速度,甚至也会包括一些其实根本不需要这些数据的处理器。

 

The following are some of the common pitfalls that can hurt overall performance:

会降低系统的整体性能的几个常见的隐患有以下几点:

  • 使用过多的线程,并且高频率的更改共享区域的数据。当多个线程试图更新加了锁的共享数据时,会有锁竞争的情况出现,从而会产生了大量的线程间的切换的开销。
  • 当因为某个线程经常分配不到充足的执行时间,所以该线程在缓存中的数据总是被其他线程的数据替换出缓存时,也会大幅降低缓存的效率。

These are some of the things application designers can do to reduce the problem:

一些可以尽可能避免这些问题的方法有:

  • 尽可能把需要多个线程共享数据的需求降到最低程度,主要方法包括在多处理器之间合理分配数据来尽可能的减少共享的区域。(OO的或者是在一些上下文无关组件(context-free components)的设计中,会经常导致比较繁琐的一些接口。)
  • 尽量使用接近系统实际支持的线程数量来尽可能避免线程间上下文的切换操作,同时也要减少每个线程阻塞的机会,比如锁,线程间任务传递,处理IOCP(IO完成端口,I/O-Completion Ports)等等。

 

我将用一个简单的提供静态web服务的情况来说明使用合理的数据分配来优化整体系统的性能,并且和一个有共享数据和锁的情况做一个对比。在这个例子中所提到的数据不仅仅是指一些负载的数据,比如缓存,或者历史页面,并且也指一些控制逻辑,比如工作队列,统计数据,freelists等等。图1中指出了一个应该尽量避免更新共享数据的一种情况。就算当负载是只读的时候,控制逻辑也是以更新为主的。

 

Thread interaction in a simple static web server scenario

Figure 1 Thread interaction in a simple static web server scenario

图1 在一个简单的静态Web服务中的线程间的交互

 

正确的做法是将数据在多处理器或者是NUMP的节点之间完全分割开。虽然在实际情况中这个很难做到,但这是系统设计的主要方向。

在理想状态下,线程的数量应该等于处理器的数量,并且每个线程的亲缘性(affinity)都对应每个处理器。每个线程都有一个独立的事件驱动的IPCP。每个处理器都要有一个网卡,每个网卡的中断请求应该用IntFilter(Win2003)或者IntPolicy(>=Win2008)来绑定到对应的处理器上。或者可以使用支持RSS(Receive Side Scaling(接收端扩展))的网卡。一个智能的网络交换机可以通过link或者是port aggregation把接收的数据分配到多个网卡上。因为负载(缓存中的历史页面)是只读的,数据可以从任意一个CPU读取。在完全的分割(包括硬盘上的数据)的情况下,需要将每个请求分配的拥有该请求所需要的数据的部分,不过这已经超出了网络交换机的能力范围了。

 

图2的设计方案是:每个线程轮询该线程的IOCP,然后当有事件发生时为其提供相应的服务。(比如一个被请求的页面不在缓存中,然后执行一个异步的读操作,同时还要将该操作的结果注册到这个线程IOCP里,当读操作完成时就可以为其提供读到数据了)。如此以来,需要被更新的数据就只剩下了一些关于管理缓存的数据了(引用计数,更新hash表,替换掉较旧的内容)。那些经常被更新的计数器也应该在每个处理器中分别更新,然后执行少量的合并操作。

 

Software and Hardware partitioning yields best performance

Figure 2: Software and Hardware partitioning yields best performance

图2: 用软件和硬件数据分割的优化达到最佳的性能

 

当分配负载时link或者是port aggregation的方法不够理想的时候,程序应该注意处理器的数量。当不同操作之间的处理时间有显著差别时,可能还需要一种达到负载均衡的方法。使用软亲和性(soft affinity)(用SetThreadIdealProcessor API)可能在某些情况下足够用了,但是如果线程是"硬亲和"(hard-affinitized)到处理器时(用SetThreadAffinityMask API),可能就要需要周期性的"抢任务"的逻辑来做到避免出现一些处理器是空闲的,但新任务却被按排在了其他处理器上的任务队列中。处理IOCP可能就更棘手了,更多的详细内容我在之后的内容详细介绍,并且我还会解释在Vista系统下应该如何使用。

 

在这个系列中的第一部分中,我们将会介绍线程和与线程相关的一些副作用,比如有过多的活动线程争抢相同资源或者是试图更新同一块内存。

还会大概介绍Vista在线程处理的一些方面的提高。

 

Threading Issues

线程问题

 

一个程序有太多的活动线程是一件坏事,尤其是当共享的数据被频繁的更新的时候,因为需要用锁来保护这些共享的数据。

当经常发生锁的动作的时候,就算是持有锁的操作所需要的时间很短,但每个线程还是都会在被相应的锁阻塞之前执行一小段时间。

当其他的任意一个线程再次开始执行时,在缓存中的关于这个线程的状态信息已经被完全替换掉了。

同时,在更多情况下是当持有一个锁的时候preemption。一个好的设计者永远都不会让持有锁的操作执行一个可能会导致阻塞的操作,因为这样有可能会增加持有锁的时间。不幸的是,不是所有的操作都能被开发者们控制,比如preemption和page faults这些不能被开发者控制的操作也会阻塞。

 

Guidelines for reducing the number of threads

减少线程数的一些宗旨

 

一般程序使用过多线程是为了简化代码,而不是想要并行计算。

一个经典的反教材就是当一个函数调用就可以完成的任务,却用一个线程递交给另一个线程一个任务,然后等待另一个线程完成任务。

不过这也有一些需要分离出consumer线程有一些特殊情况,比如consumer线程是在另一个进程中,或者是其他特别需求等等。

不过就算是这种情况,consumer的操作应该是异步的,因为conumser可能会因为某种原因没有反应而导致阻塞。

 

另外一个使用过多的线程原因是因为没有很好的利好用异步I/O。在很多情况中,"lazy-write"或者是"read-ahead"线程是不必要的,执行一个异步的I/O操作,然后在主线程中来处理完成就可以了。

 

开发者对于一些其他的原因可能没有办法,比如:当需要在一个"unified state machine"上创建一个输入处理(input handler)线程来处理IOCP或者是RPC。而且,如果使用了多个组件(RPC,COM,...)的话可能会造成程序中存在很多个线程池(thread-pools),而且每个线程池在执行thread-throttling的时候,不会察觉到其他线程池的存在。

 

在理想状态下,一个程序应该每个处理器有一个线程,而且每个线程永远都不阻塞。在实际情况中,避免发生阻塞其他线程的情况几乎是不可能的。一种折衷的方法是每个处理器有一个永远都不会调用阻塞方法的主线程。把可能会使线程阻塞的任务递交给线程池中的一个线程。这样的话,就算线程池中的一个线程阻塞了,主线程也一直可以工作。

 

 

一些关于设计程序的线程池的建议

Design recommendations for an application thread pool

 

常用的使用线程的方法

  • 完成端口和thread-throttling (Completion Port Thread Throttling)
  • 在请求之间或者是执行期间切换线程 (Switch Threads between Requests or During  )
  • 处理多个输入信号(信令) (Handling Multiple Input Signals)
  • 操作系统底层线程调度的基础 (OS Thread Scheduling Basics)
  • 超线程(HyperThreading) (HyperThreading Specifics)

 

Stay tuned for our next installment "Data Structures and Locking Issues" ...

接下来在下一篇中,将要讲到的是 "数据结构和锁" (Data Structures and Locking Issues)

 

-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·- 分 割 线 -·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-

 

虽然是3年前的文章了,不过还是有些帮助,文章里用的Vista举得例子,貌似Windows7对Vista又有不少的提升,等这三篇都翻译完了之后,再好好看看相关文档。知道了一些关于Windows API的东西之后才更好的了解了C#里的库为什么是那么设计的了。

 

MS Windows这个大家都很鄙视又对他表示很无奈的操作系统还是有很多东西可以学的。

 

这一节的后面部分还没翻译完,以后再更新... 后面两篇也会更新, 貌似是第一次翻译,欢迎指正任何错误。谢谢^_^

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值