• Feeds

  • Archive for April, 2009


    高性能网站经验-读杨建的Blog有感

    今天拜读了杨建的博客, 杨建开发的程序以高请求,高带宽为主,比如:

    开发的一系列系统中的两个 并发达到54.15w req/s , connections 340.25w  高峰一小时近20亿实际http请求处理量。

    所以整理了一些观点如下,喜欢吃快餐的请进。由于整理下面内容是跨十几篇文章的,就不一一给出链接了,需要看原文的简单Google一下即可找到。

    一 、如何衡量Web Server的性能指标

    总体来说同时在线connections和当时的每秒请求处理量是两个最重要的指标。

    实验环境数据: 杨建曾写了个HTTP服务框架,不使用磁盘I/O,简化了逻辑处理部分,只会输出 “hello world!”  程序部署在192.168.0.1上(2cup*4Core,硬件和系统都做过优化),在另外8台同等配置服务器上同时执行命令  ./apache/bin/ab -c 1000  -n 3000000   -k  “http://192.168.0.1/index.html” 几乎同时处理完毕,总合相加 40w req/s,他相信这是目前硬件水平上的极限值 。

    二、部署经验

    I. 对于cache处理的创新

    通过在url后面增加?maxage=xxx的做法,服务器通过这个参数来返回cache失效的maxage,比较灵活,优点:

    1. 可以控制HTTP header的的 max-age 值。
    2. 让用户为每个资源灵活定制精确的cache时间长度。
    3. 可以代表资源版本号。

    好奇,试验了一下杨建的系统
    $ curl -i http://sports.sinajs.cn/today.js?maxage=22

    HTTP/1.1 200 OK
    Server: Cloudia
    Last-Modified: Tue, 28 Apr 2009 14:10:02 GMT
    Cache-Control: max-age=22
    Content-Encoding: deflate
    Content-Length: 257
    Connection: Keep-Alive
    Content-Type: application/x-javascript

    大家可以看看返回的字段,其中有一点很有趣,返回直接不理http请求强制返回deflate(zip)格式, 比较霸道。

    II. IDC分布

    关于网络环境与IDC分布,正在迅速成长的公司可以了解下原文杨建介绍的经验,非常详尽。为什么说迅速成长的公司需要看,因为大的网络公司已经很清楚了,小的公司估计也用不上 :)

    III. DNS解析经验

    DNS解析有有个缺陷,每个单独域名里写在最前面的那个ip,它被轮询到的概率要比同组的服务器高10%,而且随着同组服务器的增多,这个差距会变大。所以最解析时候,每个IDC最好把硬件性能最好的服务器ip放在最前面。

    IV. 优化网卡, 调整内核参数

    这两段介绍也很有价值,主要是一些参数调优,做大型系统的推荐去看一下原文

    三、开发经验

    I. 关于epoll

    epoll最擅长的事情是监视大量闲散连接,批量返回可用描述符,这让单机支撑百万connections成为可能。
    边缘触发ET 和 水平触发LT 的选择:
    早期的文档说ET很高效,但是有些冒进。但事实上LT使用过程中,苦恼了将近一个月有余,一不留神CPU 利用率99%了,可能是我没处理好。后来zhongying同学帮忙把驱动模式改成了ET模式,ET既高效又稳定。
    简单地说,如果你有数据过来了,不去取LT会一直骚扰你,提醒你去取,而ET就告诉你一次,爱取不取,除非有新数据到来,否则不再提醒。

    自己用epoll写C的可以去深入了解下。

    II. 写数据

    顺便再说下写数据,一般一次可以write十几K数据到内核缓冲区。
    所以对于很多小的数据文件服务来说,是没有必要另外为每个connections分配发送缓冲区。
    只有当一次发送不完时候才分配一块内存,将数据暂存,待下次返回可写时发送。
    这样避免了一次内存copy,而且节约了内存。

    III. 如何节约CPU

    HTTP请求预处理 (预压缩,预取lastmodify,mimetype)
    预处理,原则就是,能预先知道的结果,我们绝不计算第二次。

    预压缩:我们在两三年前就开始使用预压缩技术,以节约CPU,伟大的微软公司在现在的IIS 7中也开始使用了。所谓的预压缩就是,从数据源头提供的就是预先压缩好的数据,IDC同步传输中是压缩状态,直到最后web server输出都是压缩状态,最终被用户浏览器端自动解压。

    IV. 怎样使用内存

    1. 避免内核空间和用户进程空间内存copy (sendfile, splice and tee)
    sendfile: 它的威力在于,它为大家提供了一种访问当前不断膨胀的Linux网络堆栈的机制。这种机制叫做“零拷贝(zero-copy)”,这种机制可以把“传输控制协议(TCP)”框架直接的从主机存储器中传送到网卡的缓存块(network card buffers)中去,避免了两次上下文切换。据同事测试说固态硬盘SSD对于小文件的随机读效率很高,对于更新不是很频繁的图片服务,读却很多,每个文件都不是很大的话,sendfile+SSD应该是绝配。

    2. 内存复用  (有必要为每个响应分配内存 ?)
    对于NBA JS服务来说,我们返回的都是压缩数据,99%都不超过15k,基本一次write就全部出去了,是没有必要为每个响应分配内存的,公用一个buffer就够了。如果真的遇到大数据,我先write一次,剩下的再暂存在内存里,等待下次发送。

    V. 减少磁盘I/O

    共享内存可以考虑用BDB实现,它取数据的速度每秒大概是100w条(2CPU*2Core Xeon(R) E5410 @ 2.33GHz环境测试,单条数据几十字节),如果你想取得更高的性能建议自己写。

    Lua coroutine vs Java wait/notify

    在上文Lua coroutine 不一样的多线程编程思路 中想到coroutine的运行机制跟Java中的wait/notify很相似,所以写了一个简单程序比较一下。

    源代码

    Lua code

    co = coroutine.create(function(loops)
        for i = 1, loops do
            coroutine.yield()
        end
    end)
    
    local x = os.clock()
    local loops = 100 * 1000 * 1000
    coroutine.resume(co, loops)
    for i = 1, loops do
        coroutine.resume(co)
    end
    print(string.format("elapsed time: %.2f\n", os.clock() - x))

    Java code

    public class TestWait {
        public static void main(String[] args) {
            WaitClass wc = new WaitClass();
            wc.start();
            int loops = 100 * 1000 * 1000;
            long t1 = System.currentTimeMillis();
            for (int i = 0; i < loops; i++) {
                synchronized (wc) {
                    wc.notify();
                }
            }
            long t2 = System.currentTimeMillis();
            System.out.println("elapsed time: " + (t2 - t1) / 1000l);
        }
    }
    
    class WaitClass extends Thread {
        public void run() {
            while (true) {
                synchronized (this) {
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    运行结果

    Lua elapsed time: 53.36
    Java elapsed time: 51

    CPU占用

    运行环境:4core XEON

    Lua 1CPU 100%, 其他CPU0%, total 25% (其中CPU sys 0%)

    Java 2个CPU 40%-50%, 其他CPU 0%, total 25% (其中CPU sys 5%-10%)

    从结果看,coroutine只利用了一个CPU, 和原理所说完全一致。

    Java利用了2个CPU, 各占用了50%的CPU时间运行和50%的时间等待,和设计也一致。另外Java用了5-10%的sys CPU时间用于线程context switch

    结论

    虽然这两种程序没有直接可比性,但仍然可以看出一些有趣的地方:

    • Lua虽然在各种性能评比中performance比Java低一个数量级,但在这个场景中也跑平了Java
    • Java为了调用notify/wait, 用了同步锁,因此测试场景对Java不利。

    再谈coroutine应用场景

    今天又看到qiezi的文章并发编程模型:Thread, Coroutine, Callback … 分析得很深入,对这方面感兴趣的可以进一步去深入了解。

    另外qiezi在Coroutine在并发程序中的应用中提到四种场景,可以理解是对我上篇文章对coroutine应用场景的一种答案。

    1. 状态机。
    2. 异步IO操作:异步IO操作通常是发起一个IO请求,由操作系统完成以后回调指定的方法或者使用其它方式通知。
    3. 高并发网络服务器,高并发服务器除了要处理场景一的情况外,可能还要结合场景二,多线程方案有时候完全不能接受,更多的是基于事件、异步IO或者是混合事件和多线程的模型。
    4. 客户端并发应用

    但是我还是觉得存在疑虑,后面几种我觉得用多线程/线程池模式也可以很好解决。其实select/epoll异步IO方式跟多线程并不矛盾。多线程并不代表每个线程需要recv阻塞在那里。目前网络服务器的多线程通常是指业务逻辑处理部分使用多线程。比如Java中用mina来处理连接(相当于epoll),mina在收到数据包之后再分发给负责业务逻辑的thread pool处理。如果是CPU密集型任务,简单把线程池的线程数设成CPU数即可达到性能最佳。这时如果把线程数设成1,就很类似coroutine模式了。而Java模式所增加的消耗,主要是new runnable class以及线程池调度的开销。

    Lua coroutine 不一样的多线程编程思路

    上周末开始看《Lua程序设计》第二版,目前体会到其中比较有趣的有两点,一是强大的table数据结构,另外就是coroutine。也许Lua中的coroutine是一种很好的设计模式,但我初步的体会还是没想到其他语言和场合能非常适合用到coroutine的场景。

    一、简介

    协同程序与线程差不多,也就是一条执行序列,拥有自己独立的栈,局部变量和指令指针,同时又与其它协同程序共享全局变量和其它大部分东西。线程与协同程序的主要区别在于,一个具有多线程的程序可以同时运行几个线程,而协同程序却需要彼此协作地运行。就是说,一个具有多个协同程序的程序在任何时刻只能运行一个协同程序,并且正在运行的协同程序只会在其显示地挂起时,它的执行才会暂停。

    如:

    co = coroutine.create(function ()
    for i=1,10 do
    print("co", i)
    coroutine.yield()
    end
    end)

    从主线程调用
    coroutine.resume(co)
    会依次打印1到10

    二、原理探析

    • coroutine创建的所谓的“线程”都不是真正的操作系统的线程,实际上是通过保存stack状态来模拟的。
    • 由于是假的线程,所以切换线程的开销极小,同时创建线程也是轻量级的,new_thread只是在内存新建了一个stack用于存放新coroutine的变量,也称作lua_State

    LUA_API lua_State *lua_newthread (lua_State *L)

    • 调用yield()当前线程交出控制权,同时还可以通过stack返回参数。调用resume的线程(可理解为主线程)获得返回的参数。
    • Lua yield()和Java中的Thread.yield()有点相似,但是区别更大。Java中的yield调用后只是将当前CPU切换到另外一个线程,CPU可能随时会继续回到线程执行。
    • 我更倾向于把Lua中的yield()和resume()和Java中的wait()和notify()来对比。它们表现的行为基本一致。
    • 关于stack实现也可参看Yufeng(Erlang高手)的分析文章 lua coroutine是如何实现的?

    三、Why coroutine?

    上面对coroutine有个基本的了解,因此大家都会象我一样去想,为什么要用coroutine?先研究下优点

    • 每个coroutine有自己私有的stack及局部变量。
    • 同一时间只有一个coroutine在执行,无需对全局变量加锁。
    • 顺序可控,完全由程序控制执行的顺序。而通常的多线程一旦启动,它的运行时序是没法预测的,因此通常会给测试所有的情况带来困难。所以能用coroutine解决的场合应当优先使用coroutine。

    再看缺点,研究coroutine缺点之前,我寻找了一下Lua中为什么实现coroutine的一些说明。在巴西人写的paper Coroutines in Lua(pdf)中解释了几个原因:

    • Lua是ANSI C实现的,ANSI C并不包含thread的实现,因此如果要在Lua增加thread的支持就要使用操作系统本地的实现,这样会造成通用的问题。同时也会使Lua变得臃肿。因此Lua选择了在ANSI C上实现的coroutine。
    • Lua主要设计目的之一是给C调用,如果Lua内部又有多线程实现的话会造成C调用状态的混乱,而只提供coroutine层面的挂起则可以保持状态的一致性。

    以上这些理由都是基于Lua特殊的原因而使用的,并不是很通用的原因。我们也了解到,coroutine实际上是一种古老的设计模式,它在60年代就已经定型,但是现代语言很少有重视这个特性,目前可以举例的有Windows的fibers, Python的generators

    四、Lua coroutine和Erlang

    上面优点有1条没展开,就是每个coroutine有自己私有的stack及内存变量空间。因此可以认为coroutine和Erlang中的process是非常相似的。但是coroutine只能同时只有一个在执行,如果能让他多个同时跑,我觉得就和Erlang非常相似了。

    《Lua程序设计》第二版30.2介绍的一种实现方法,让多个c threads启动,然后每个c thread启动一个coroutine(类似Erlang process),然后通过stack传递变量值(类似Erlang process message),这样就可以实现一个类似Erlang的process模型了。由于coroutine实际上可以用任何语言实现,那其他语言应该也可实现同样这种设计方法。

    五、Lua其他

    Lua目前主要用在游戏编程领域,通常的观点Lua是“胶水语言”。用来把各个模块化的功能粘合起来。就我目前阅读的一些代码来看,C和Lua通常是混合在一起的,并没有明确的边界。对于我一个外行的眼光看来我分不清哪些是在做C的事情,哪些是在调用Lua。特别是这个“胶水”如果放得太多,系统中各个模块的独立性将会受到影响。比如云风的这篇Lua 不是 C++也提到,“这属于过厚的粘合层,是绝对需要抛弃的”。

    另外Code@Pig一篇[网游设计] 一点感想也提到要简化调用,我总结它的观点主要两点:

    1. 不要存在冗余的关系,给一个部分负责管理就好。(由Lua/python来管理)
    2. 粘合层(Lua/python接口)不要过胖,我们可以通过引入一个“间接层”来把粘合层做“薄”

    虽然Lua的高效和精简的设计让人赞誉有加,但是它的性能排名并不高,和Python大致在同一个级别。另外“胶水语言”的定位也妨碍了它在更多领域的发展。

    123