测试三明治和雪鸮探索测试

测试金字塔理论被广泛应用于计划和实施敏捷软件开发所倡导的测试自动化,并且取得了令人瞩目的成就。本文尝试从产品开发的角度出发,结合Kent Beck最近提出的3X模型和近年来迅速发展的自动化测试技术,提出并讨论一种新的测试层级动态平衡观:三明治模型。同时,为了应对端到端测试在实践中面临的种种挑战,设计并实现了一种面向用户旅程的端到端自动化测试框架——雪鸮。实际项目经验表明,雪鸮能够显著提升端到端测试的可维护性,减少不确定性影响,帮助开发人员更快定位和修复问题,对特定时期的产品开发活动更具吸引力。

背景

测试金字塔

按照自动化测试的层级,从下至上依次为单元测试、集成测试和端到端测试,尽量保持数量较多的低层单元测试,以及相对较少的高层端到端测试,这就是测试金字塔理论。随着敏捷软件开发的日益普及,测试金字塔逐渐为人所知,进而得到广泛应用。Mike CohnMartin Fowler以及Mike Wacker等先后对测试金字塔进行了很好的诠释和发展,其主要观点如下:

  • 测试层级越高,运行效率就越低,进而延缓持续交付的构建-反馈循环。
  • 测试层级越高,开发复杂度就越高,如果团队能力受限,交付进度就会受到影响。
  • 端到端测试更容易遇到测试结果的不确定性问题,按照Martin Fowler的说法,这种结果不确定性的测试毫无意义。
  • 测试层级越低,测试的代码隔离性越强,越能帮助开发人员快速定位和修复问题。

3X模型

2016年起,敏捷和TDD先驱Kent Beck开始在个人Facebook主页撰写系列文章,阐述产品开发的三个阶段——Explore、Expand和Extract,以及在不同阶段中产品与工程实践之间的关系问题,即3X模型。近二十年软硬件技术的飞速发展,使得软件开发活动面临敏捷早期从未遇到的市场变革,而根据在Facebook工作的经历,Kent Beck把产品开发总结为三个阶段:

  • 探索(Explore),此时的产品开发仍处于非常初期的阶段,仍然需要花费大量时间寻找产品和市场的适配点,也是收益最低的阶段。
  • 扩张(Expand),一旦产品拥有助推器(通常意味着已经找到了市场的适配点),市场需求就会呈现指数级上升,产品本身也需要具备足够的伸缩性以满足这些需求,由此收益也会快速上升。
  • 提取(Extract),当位于该阶段时,公司通常希望最大化产品收益。但此时收益的增幅会小于扩张阶段。

(3X)

Kent Beck认为,如果以产品是否成功作为衡量依据,那么引入自动化测试在探索阶段的作用就不大,甚至会延缓产品接受市场反馈循环的速度,对产品的最终成功毫无用处,还不如不引入;当位于扩张阶段时,市场一方面要求产品更高的伸缩性,另一方面也开始要求产品保证一致的行为(例如质量需求),那么此时就该引入自动化测试来保证产品的行为一致性;当产品最终处于提取阶段时,任何改动都应以不牺牲现有行为为前提,否则由此引发的损失可能远高于改动带来的收益,此时自动化测试就扮演了非常重要的角色。

测试工具爆炸式增长和综合技能学习曲线陡升

根据SoftwareQATest网站的历史数据,2010年记录的测试工具有440个,共划分为12个大类。这个数字到2017年已经变为560个,共15个大类,且其中有340个在2010年之后才出现。也就是说,平均每年就有50个新的测试工具诞生。

面对测试工具的爆炸式增长,一方面所支持的测试类型更加完善,更加有利于在产品开发过程中保证产品的一致性;另一方面也导致针对多种测试工具组合的综合技能学习曲线不断上升。在实践中,团队也往往对如何定义相关测试的覆盖范围感到不知所措,难以真正发挥测试工具的效用,也很难对产品最终成功作出应有的贡献。

从金字塔到三明治

作为敏捷在特定时期的产物,测试金字塔并不失其合理性,甚至还对自动化测试起到了重要推广作用。但是,随着行业整体技术能力的不断提升,市场需求和竞争日趋激烈,在项目中具体实施测试金字塔时往往遭遇困难,即便借助外力强推,其质量和效果也难以度量。

此外,随着软件设计和开发技术的不断发展,低层单元测试的传统测试技术和落地,因前、后端技术栈的多样化而大相径庭;同时,在经历过覆盖率之争,如何确保单元测试的规范和有效,也成为工程质量管理的一大挑战;高层的端到端测试则基本不受技术栈频繁更替的影响,随着不同载体上driver类技术的不断成熟,其开发复杂度反而逐渐降低。

这里讨论一种新的测试层级分配策略,我们称之为三明治模型 。如下图所示,该模型允许对不同测试层级的占比进行动态调整,说明了倒金字塔形、沙漏形以及金字塔形分配对特定产品开发阶段的积极作用。

(Sandwich)

产品开发的自动化测试策略

根据3X模型,在探索初期往往选择避开自动化测试。一旦进入扩张期,产品的可伸缩性和行为一致性就成为共同目标,但此时也常会发生大的代码重构甚至重写,如果沿用测试金字塔,无论补充缺失的单元测试,还是只对新模块写单元测试,都既损害了产品的快速伸缩能力,也无法保证面向用户的产品行为一致性。因此,如果在探索后期先引入高层的端到端测试,覆盖主要用户旅程,那么扩张期内所产生的一系列改动都能够受到端到端测试的保障。

需要注意的是,用户旅程在产品即将结束探索期时通常会趋于稳定,在扩张期出现颠覆性变化的概率会逐渐减少,端到端测试的增长率会逐步下降。

除此以外,随着扩张期内不断产生的模块重构和服务化,团队还应增加单元测试和集成测试的占比。其中,单元测试应确保覆盖分支场景(可以在CI中引入基于模块的覆盖率检测);集成测试和某些团队实践的验收测试,则需进一步覆盖集成条件和验收条件(在story sign-off和code review时验收)。

许多新兴的测试技术和工具擅长各自场景下的验收测试,但更重要的仍是识别产品阶段和当前需求,以满足收益最大化。

(Sandwich-3x)

由此我们认为,随着产品开发的演进,测试层级的分配应参考三明治模型,动态调整层级占比,更加重视运营和市场反馈,致力于真正帮助产品走向成功。

端到端测试的机遇和挑战

与其他测试层级相比,端到端测试技术的发展程度相对滞后。一方面,作为其基础的driver工具要在相应载体成熟一段时间之后才能趋于稳定,web、mobile无不如是。另一方面,端到端测试偏向黑盒测试,更加侧重描述用户交互和业务功能,寻求硬核技术突破的难度较高,于是较少受开发人员青睐。但是,由于端到端测试更接近真实用户,其在特定产品开发活动中的性价比较高,有一定的发展潜力。

然而,当前实践中的端到端测试,普遍存在如下问题:

  • 低可维护性。一般实践并不对测试代码质量作特别要求,而这点在端到端测试就体现得更糟。因为其涉及数据、载体、交互、功能、参照(oracle)等远比单元测试复杂的broad stack。虽然也有Page Object等模式的广泛应用,但仍难以应对快速变化。
  • 低运行效率。如果拿单次端到端测试与单元测试相比,前者的运行效率肯定更低。因此只一味增加端到端测试肯定会损害构建-反馈循环,进而影响持续交付。
  • 高不确定性。同样因为broad stack的问题,端到端测试有更高的几率产生不确定测试,表现为测试结果呈随机性成功/失败,进一步降低运行效率,使得真正的问题很容易被掩盖,团队也逐渐丧失对端到端测试的信心。
  • 难以定位问题根因。端到端测试结果很难触及代码级别的错误,这就需要额外人工恢复测试环境并尝试进行问题重现。其中所涉及的数据重建、用户交互等会耗费可观的成本。

方法

为了解决传统端到端测试遇到的种种挑战,本文设计了一种面向用户旅程的端到端自动化测试框架——雪鸮(snowy_owl),通过用户旅程优先、数据分离、业务复用和状态持久化等方法,显著提高了端到端测试的可维护性,降低不确定性的影响,并且能够帮助团队成员快速定位问题。

用户旅程驱动

端到端测试应尽量贴近用户,从用户旅程出发能保证这一点。在雪鸮中,用户旅程使用被称作play books的若干yaml格式的文件进行组织,例如下列目录结构:

play_books/
  core_journey.yml
  external_integration.yml
  online_payment.yml

其中每个play book由若干plots所组成,plot用于表示用户旅程中的“情节”单位,其基本特征如下:

  • 单一plot可以作为端到端测试独立运行,例如发送一条tweet的情节:
SnowyOwl::Plots.write 'send a plain text tweet' do
  visit '/login'  
  page.fill_in 'username', with: 'username'
  page.fill_in 'password', with: 'password'
  page.find('a', text: 'Sign In').click
  # verify already login?
  page.find('a', text: 'Home').click
  # verify already on home page?
  page.fill_in 'textarea', with: 'Hello World'
  page.find('a', text: 'Send').click
  # verify already sent?
end
  • 单一plot应是紧密关联的一组用户交互,并且具备体现一个较小业务价值的测试参照。
  • plot可以被play book引用任意次从而组成用户旅程,play book同时定义了所引用plots之间的顺序关系,基本语法如下所示:
---
- plot_name: send a plain text tweet
  digest: 2aae6c35c94fcfb415dbe95f408b9ce91ee846ed
  parent: d6b0d82cea4269b51572b8fab43adcee9fc3cf9a

其中plot_name表示情节的标题,digest和parent分别表示当前情节引用在整个端到端测试过程中的唯一标识和前序情节标识,初期开发人员可以通过各个情节的引用顺序定义用户旅程,大多数情况下digest和parent将由系统自动生成并维护。

整个play books集合将是一个以plots为基础组成的森林结构,而端到端测试的执行顺序则是针对其中每棵树进行深度遍历。

通用业务复用

由于plot本身必须是一个独立可运行的端到端测试,那么plots之间通常会共享一部分交互操作,例如用户登录。雪鸮允许把高度可复用的交互代码进行二次抽取,称作determination:

SnowyOwl::Determinations.determine('user login') do |name, user_profile|
  # return if already login
  visit '/login'  
  page.fill_in 'username', with: user_profile[:username]
  page.fill_in 'password', with: user_profile[:password]
  page.find('a', text: 'Sign In').click
  # verify already login?
end

这样,plot的代码就可以简化成:

SnowyOwl::Plots.write 'send a plain text tweet' do
  determine_user_login({username: 'username', password: 'password'})
  page.find('a', text: 'Home').click
  # verify already on home page?
  page.fill_in 'textarea', with: 'Hello World'
  page.find('a', text: 'Send').click
  # verify already sent?
end

这里应注意Determination和Page Object的区别。看似使用Page Object可以达到相同的目的,但是后者与Page这一概念强绑定。而Determination更加侧重描述业务本身,更符合对用户旅程的描述,因此比Page Object在plot中更具适用性。当然,在描述更低层的组件交互时,Page Object仍然是最佳选择。

测试数据分离

合理的数据设计对描绘用户旅程非常重要,雪鸮对测试逻辑和数据进行了进一步分离。例如用户基本数据(profile),同样是使用yaml文件进行表示:

data/
  tweets/
    plain_text.yml
  users/
    plain_user.yml

那么在plot的实现中,就可以使用同名对象方法替代字面值:

SnowyOwl::Plots.write 'send a plain text tweet' do
  determine_user_login({username: plain_user.username, password: plain_user.password})
  page.find('a', text: 'Home').click
  # verify already on home page?
  page.fill_in 'textarea', with: plain_text.value
  page.find('a', text: 'Send').click
  # verify already sent?
end

情节状态持久化

雪鸮的另一个重要功能是情节状态的持久化和场景复原。为了启用情节状态持久化,开发人员需要自己实现一个持久化脚本,例如对当前数据库进行dump,并按照雪鸮提供的持久化接口把dump文件存储至指定位置。

当端到端测试运行每进入一个新的情节之前,系统会自动执行持久化脚本。也就是说,雪鸮支持保存每个情节的前置运行状态。

当端到端测试需要从特定的情节重新开始运行时,雪鸮同样会提供一个恢复接口,通过用户自定义的数据恢复脚本把指定位置的dump文件恢复至当前系统。

该功能有两处消费场景:

  • 由于broad stack的问题,端到端测试不确定性的技术因素一般较为复杂。实际经验表明,测试的随机失败率越低,就越难以定位和修复问题,而通过不断改进测试代码的方式消除这种不确定性的成本较高,效果也不好。但是,可以尽量消除不确定性带来的影响。例如,不确定测试导致的测试失败,通常会导致额外人工验证时间,完全可以选择让系统自动重试失败的测试。另一方面,重试会造成测试运行效率降低,特别是针对端到端测试。当一轮端到端测试结束后,雪鸮只会自动重试失败的情节测试,同时利用该情节对应的数据dump文件保证场景一致性,这就减少了重试整个端到端测试带来的运行效率下降问题。
  • 当团队成员发现端到端测试失败,通常需要在本地复现该问题。而借助测试dump文件,可以直接运行指定plot测试,从而避免额外的人工设置数据和交互操作,加快问题定位和解决。

实践

雪鸮在笔者所在的项目有超过6个月的应用时间。该项目在产品开发方面长期陷入困境,例如过程中同时兼具了3X每个阶段的特点,不仅缺少清晰的产品主线,还背负了接棒遗留系统的包袱。这种状况对工程质量管理提出了更大挑战。

项目采用雪鸮对已有端到端测试进行了重构,生成了一个核心用户旅程和三个涉及外部系统集成的重要用户旅程,包含24个plots,9个determinations,使端到端测试实现了长期稳定运行。在本地相同软硬件环境下,不确定性导致的随机失败从原有10%降低至1%以内,部署至云环境并采用headless模式后,连续15天测试失败记录为零,运行效率的损失可以忽略不计。同时,当用户旅程产生新分支时,可以引入新的情节测试节点,并且根据业务需求将其加入现有play book树,从而实现端到端测试的快速维护。

持续集成与常态化运行

项目完整的端到端测试的平均运行时间保持在19分钟左右,为了不影响现有持续集成节奏,CI每30分钟自动更新代码并运行端到端测试,结果在dashboard同步显示,一旦发生测试失败,第一优先级查找失败原因并尝试在本地复现和修复。

常态化运行端到端测试的另一个好处是,能够以低成本的方式实现24小时监控系统各个组件的功能正确性,有助于更早发现问题:一次,产品即将上线的支付功能发生异常,查看CI记录发现端到端测试在晚上9:15左右出现了首次告警。通过及时沟通,确认是海外团队在当时擅自改动了支付网关的一个配置,造成服务不可用的问题,并迅速解决。

结论与展望

Kent Beck的3X模型,提出了从不同产品开发阶段看待工程实践的新视角。而敏捷一贯推崇的TDD等实践,更多体现在个人技术专长(Expertise)方面,与产品是否成功并无必然联系。然而,程序员的专业主义(Professionalism)的确同时涵盖了技术专长和产品成功两个方面,二者相辅相成。因此,如何通过平衡众多因素并最终提高整体专业性,这才是软件工程面临的经典问题。本文给出的测试三明治模型,目的就是帮助思考产品开发过程中测试层级间的平衡问题。

为了应对现有端到端测试面临的挑战,本文设计并实现了一种新的面向用户旅程的端到端测试框架,通过职责隔离、业务复用和状态持久化等手段,构建了易于维护且更加有效的端到端测试。同时,基于上述方法构建的测试代码,更易于和自动化测试的其他研究领域相结合,在诸如测试数据构建、用例生成、随机测试和测试参照增强等方向有进一步的应用潜力。


更多精彩洞见,请关注微信公众号:思特沃克

Share

微情境(Micro-Situation)——VR服务化产品之路

近年火爆的各类xR技术对大众来说是“黑科技”,其实早在50年前[1]就被人玩剩了,它的创作者是计算机图形学领域的祖师之一——Ivan Sutherland。

图1 最早的头戴式立体显示设备: Ivan E. Sutherland, A head-mounted three dimensional display, 1968

Sutherland借助线框模型表现出虚拟环境,并且把显示设备绑定在机械臂上以追踪用户视角,这些基本的VR功能直至今日都没有多少变化。在当时计算机图形学基础还没有完全确立的情况下,也许是发现自己已经走得太远,Sutherland带着他一票学生和下属回去后方铺路,他们先后发明过Smalltalk编程语言、Gouraud着色算法、图形反走样技术,创办过创意设计巨头Adobe、高性能计算鼻祖SGI和动画界翘楚Pixar[2]

有意思的是,多年来,xR技术在理论领域的研究早已从感知交互延伸至哲学范畴……然而在实践上,却长期停留在科技馆和特殊行业。2010年起,领先的电子游戏团队开始把目光转向VR研发,其中包括John Carmack和著名的Valve公司,这使VR技术在短短几年内得以进入消费领域。2014年开始,Facebook、Sony、Google等巨头纷纷加入,不仅使VR这个陈年旧词终于复兴,还一并推动了AR、MR等相关领域的发展。

图2 xR技术的发展

AR vs VR vs MR

AR和VR分别依赖不同的支撑技术,其应用的细分领域也不相同。众所周知,在这波VR行情之前,智能手机平台上就已经出现了各种炫目的AR应用,但大多都如Google Glass一样昙花一现,始终没有真正的杀手级应用出现(多年也才蹦出一只Pokemon Go……)。与AR相比,VR最大的不同就是有坚实的游戏产业做后盾,堪称惊艳的游戏作品在各大VR平台上层出不穷。但由于VR游戏的门槛较传统PC游戏大作更高,因此也只有数量有限的发烧玩家才愿意为之买单。

一向掌握“黑科技”的微软,也不甘寂寞地向世界展示了自家的Hololens,这的确是当前无与伦比的混合现实(MR)平台。然而,Hololens同时也拥有比任何一款VR/AR平台都高的门槛,目前也只能面向专业领域。

必须承认,无论是AR、VR、MR,当前都还需要更长期的技术探索才有可能成为通用应用技术。然而对于更大范围的受众而言,近年来VR像是只存在于科技新闻中,根本无法想象其成为眼前实实在在的服务。信息爆炸的尘埃褪去,眼前呈现的是无比亲民的头显(HMD)和各种奇思妙想的控制器(Controller),再加上持续走低的消费门槛,无一不体现着以人为本的时代理念。过去的50年间,xR始终带着“黑科技”的标签。但是,如何解决“低门槛”和“大众化”这两大主要痛点,才是当今时代的主题。

为什么是“微情境”?

微情境最初来源于教育领域,指一种由案例教学发展而来的 主动学习工具。教师通过在既有教学案例中设置一系列细粒 度的假设分析,为学生提供多角度的学习方式。在资源受限的 条件下,微情境方法能激发以学生为中心的主动式学习,从而 获得相较于讲授式教学更好的教学效果。对于同样资源受限的VR行业,“低门槛”是首要的考虑因素。以Google Daydream为例,在如今诸多VR平台中,Daydream的优势是非常易见的:它太容易接近大众了。相较而言,其它竞品则更多聚焦在高端场合和行业应用中,不利于小团队做最小化可行产品(MVP)和持续创新。

图3 Google Daydream

Daydream的劣势目前也很明显:3-DOF的控制器无法捕捉用户的全身动作,反馈缺失导致了沉浸感不足[3];另外,移动设备受限的性能、参差不齐的规格也注定会导致一定的用户体验问题,这恐怕也是Google率先携Pixel发布Daydream的初衷。但必须清醒地认识到,移动端VR的限制在短时期内无法得到彻底解决。因此在设计VR乃至xR产品时,首先需要考虑平台对应用场景的制约,否则再精妙的创意也难被更多人接纳。

另一方面,“低门槛”不意味着“零门槛”。今天市场中充斥了模仿Cardboard或GearVR、且基本没有优质资源的头显/控制器,就是一个很好的反例。虽然理论上xR终究会成为通用应用技术,然而在当下,市场教育、人才培养和产品试错都是摆在整个行业面前的任务,微情境应用恰好是一个切入点。

微情境案例

Grigore Burdea曾提出,VR不仅应具备沉浸(Immersion)和交互(Interaction)两大基本特性,更重要的是能激发受众想象(Imagination),使其尽可能把感知到的“虚拟”事物联想为真实世界[4],这就是VR著名的3I特征。近年来随着人工智能技术的发展,有学者把3I扩展为4I,即增加了智能(Intelligence)[5]。时至今日,4I应成为每个服务化VR产品的基本立足点。

微情境的VR服务化产品设计按照3I——4I分为两个阶段持续演进,也就是说,应首先强调3I:沉浸、交互、想象,而将第四个“I”——智能视为进一步可选的细分领域。微情境的核心在于“低门槛”和“大众化”,下文以VR应用为例介绍几个微情境的案例。

案例1 教育培训

VR在现代职业/非职业教育培训中有广阔的应用前景。例如烹饪学习,传统多是一对一的学徒模式,随着媒体技术的发展,逐渐拓展成报刊书籍、广播电视式的单向学习-单向实践模式。学徒模式的好处是互动性强,学习-实践-反馈并举,学员上手更快,但人工成本很高。后者虽然省却了额外的人工成本,但缺乏互动和实践性,反馈周期长,导致学员难以切实获得做饭的能力,只是因为时间较为灵活成为绝大多数人的第一选择。

从烹饪这一微情境本身来说,用户只需要一间理想的厨房,摆放整齐的食材和调味品,再加上面前一座灶台即可,而交互方式仅需要双手参与。诸如GearVR、Daydream这样的移动端VR平台就完全满足需求,2015年在GearVR上推出的CyberCook Taster,就是一款不错的VR烹饪培训应用。

图4  Cyber Cook Taster, GearVR

在应用价值方面,VR烹饪虽然存在一些劣势如无法获得画面和声音之外的其它有效反馈包括直接品尝自己的学习成果等但它有节省人工、食材、时间使学员能够切身经历每一道工序的优势。这是现有的学习形式都不具备的。

案例2 产品说明/教程

相信大家对各式各样的产品教程都不陌生,但从现实的生活经验来看,在大多数情况下,要么产品自身完全做到了自说明,要么很难快速、或者根本无法从说明书中找到自己需要的答案。多年来,尽管平面设计不断进步,多媒体新招层出不穷,也很难找出一个真正有效的用户-产品沟通方式,而VR的普及将为此带来全新的维度。

Valve在2016年推出的免费游戏--The Lab开头就提供了一个非常经典的VR交互说明,原本看似复杂的HTC Vive控制器,仅通过一个数分钟的机器人引导就得到了充分的展示,这种采用VR制作VR教程的方式令人非常印象深刻。

图5 The Lab, HTC Vive

IKEA VR Experience是一款展示厨房家具的VR体验应用,虽然只是一个Demo,却在刚一推出时就受到了广泛关注。试想一下,假如你通过IKEA网购了一套书柜,并且需要自己动手安装。虽然IKEA提供了贴心的视频安装指南,原本自信满满的你却花费了很长时间也没成功,最终无奈地瘫坐在沉甸甸的零件旁,满头大汗地重新研究安装说明……在当前微情境中,家具的各个部件都可以被虚拟化,用户并不需要经常在场景中来回移动,而是直接上手了解零件功能和安装步骤即可。而如果IKEA把VR体验和VR安装指南同时发布在网站上,就能显著增强顾客体验。

案例3 体验式广告

体验式广告的最大不同,就是摒弃了传统广告的单向传播形式,强调观众参与、互动和切身经历,近年来逐渐被各个大厂接受并用于塑造和提升品牌价值。2016年,奥利奥推出了首个VR体验式广告“奥利奥奇幻宝库”,观众使用Cardboard就能欣赏动画版奥利奥最新产品的制作过程,制作精良的VR影片吸引了大量观众。然而对于VR体验式广告来说,这顶多算是一次试水。

 

同样是2016年,Steam上出现一款受到关注的VR游戏--EverestVR,在业界和玩家之间引发了截然不同的反应。在业界看来,玩家能不费吹灰之力,从珠峰大本营出发、穿越昆布冰瀑、在4号营地过夜、攀登希拉里台阶并最终登顶珠峰——全程仅需花费15美元,绝对堪称超值。然而对于历来挑剔的资深游戏玩家来说,乏善可陈的游戏过程和简短的游戏时间,与其15美元的“不菲价格”相比简直可以用“坑爹”来形容。平心而论,EverestVR的制作技艺实属上乘,但要想在游戏界走的更远,还需要大量的策划和内容才能满足玩家胃口。

图5 EverestVR, HTC Vive

实际上,以EverestVR同样的技艺水平甚至允许稍加简化,在体验式广告领域可能就会完全不同。例如旅游景区推广,在VR风靡之初,虚拟旅游就进入了大众视野,但人们错误地认为虚拟旅游能取代实景旅游——这种理念本身就与“旅游”的本质背道而驰。对于景区的体验式广告来说,只需要建立标志性场景的VR体验就足够惊艳了,前提是重质不重量,EverestVR完全能够作为一个衡量标准。

精益思维

随着移动端OS日趋稳定,越来越多的竞争者转而加入到xR平台的争夺中,xR平台商作为技术旗舰要面临的深层技术问题比过去任何平台都复杂得多,前段时间围绕Magic Leap掀起的风波就能令人可见一斑。平台的长期混战足以吓阻更多人进入xR领域,反倒滋生出一堆轻工业垃圾,这种大规模的资源浪费并不能对xR的普及产生任何帮助。

基于“微情境”的VR服务化产品,其依赖的开发过程并不会与当前的互联网产品开发有多少不同,精益思维依然重要。而目前的VR创业团队,仍然主要以游戏从业人员或高端3D制作人员为主,这些人才往往擅长设计游戏关卡、构建游戏场景、制作3D素材、熟悉引擎开发,但是游戏本身的艺术性需求和封闭属性,与低门槛、大众化的“微情境”产品有很大不同,因此可以说互联网行业本身就具备VR产品研发的先天优势。

对VR感兴趣的个人和团队,应全面熟悉各家平台的优劣,及时跟进并掌握行业趋势,然后结合自身优势选择可移植的落地方案。

笔者曾与同事合作开发过一款基于WebVR的应用——来自22世纪的程序员(C22)。C22是一个基于传统PC+Cardboard的微情境VR应用,它展示出未来程序员能够摆脱固定显示器的束缚,使用HMD、键鼠控制器就可以编写代码、进行日常工作。C22最特殊之处是其完全基于JavaScript语言编写,且仅需要少量3D基础知识。随着WebVR逐渐被Chrome和Firefox等主流浏览器支持,“微情境”能够帮助VR团队思考如何面对更大范围的市场和人才储备。

图6 基于传统PC+Cardboard的微情境VR应用

2015年的Proto颁奖大会上,年近八旬的萨大爷获颁虚拟现实“创始人奖”,大神激励台下一帮天才后辈说:“内容意味着一切,摄像头并不创造内容,只有最伟大的创造者才得以使用技术把现实带给人们”。

闻者深受共鸣。


  1. The Sword of Damocles,1965。
  2. 现任Pixar/Disney动画总裁的Edwin Catmull也是图形学泰斗,曾发明了非常实用的Catmull-Rom样条函数,直到现在还被广泛用于关键帧插值和渲染头发丝。(http://www.cemyuksel.com/research/catmullrom_param)。
  3. 基于Valve核心技术的产品如HTC Vive的这一优势在消费市场上还无人匹敌。
  4. Grigore Burdea等. Virtual Reality Technology, 1994.
  5. Abdulrahman M Al-Ahmari等. Development of a virtual manufacturing assembly simulation system, 2016.

更多精彩商业洞见,请关注微信公众号:ThoughtWorks商业洞见

Share

Ruby Web服务器:这十五年

坦率的说,作为一门年轻的计算机语言,Ruby在最近二十年里的发展并不算慢。但如果与坐拥豪门的明星语言们相比,Ruby就颇显平民范儿,表现始终不温不火,批评胜于褒奖,下行多过上扬。但总有一些至少曾经自称过Rubyist的程序员们,愉快地实践了这门语言,他们没有丝毫的歧视习惯,总是努力尝试各家之长,以语言表达思想,用基准评判高下,一不小心就影响了整个技术发展的进程。本文谨以Ruby Web服务器技术的发展为线索,回顾Ruby截至目前最为人所知的Web领域中,重要性数一数二的服务器技术的发展历程,试图帮助我们了解过去,预见未来。timeline

Ruby Web服务器发展时间轴

一、随波逐流

长久以来,任何Web服务器都具备的两项最重要的功能:一是根据RFC2616解析HTTP/1.1协议,二是接收、处理并响应客户端的HTTP请求。幸运的是Web技术的发展并不算太早,使得Ruby恰好能赶上这趟顺风车,但在前期也基本上受限于整个业界的进展。像Apache HTTP Server、Lighttpd和Nginx这些通用型Web服务器+合适的Web服务器接口即可完成大部分工作,而当时开发者的重心则是放在接口实现上。

cgi.rb

作为Web服务器接口的早期标准,CGI程序在调用过程中,通过环境变量(GET)或$stdin(POST)传递参数,然后将结果返回至$stdout,从而完成Web服务器和应用程序之间的通信。cgi.rb是Ruby官方的CGI协议标准库,发布于2000年的cgi.rb包含HTTP参数获取、Cookie/Session管理、以及生成HTML内容等基本功能。

Web服务器和CGI

Web服务器和CGI

当支持CGI应用的Web服务器接到HTTP请求时,需要先创建一个CGI应用进程,并传入相应的参数,当该请求被返回时再销毁该进程。因此CGI原生是单一进程/请求的,特别是每次请求时产生的进程创建/销毁操作消耗了大量系统资源,根本无法满足较高负载的HTTP请求。此外,CGI进程模型还限制了数据库连接池、内存缓存等资源的复用。

对于标准CGI应用存在的单一进程问题,各大厂商分别提出了兼容CGI协议的解决方案,包括网景的NSAPI、微软的ISAPI和后来的Apache API(ASAPI)。上述服务器API的特点是既支持在服务器进程内运行CGI程序,也支持在独立进程中运行CGI程序,但通常需要在服务器进程中嵌入一个插件以支持该API。

Webrick

作为最古老的Ruby Web服务器而不仅仅是一个接口,诞生于2000年的Webrick从Ruby 1.9.3(2011年10月正式发布)起被正式纳入标准库,成为Ruby的默认Web服务器API。Webrick支持HTTP/HTTPS、代理服务器、虚拟主机服务器,以及HTTP基础认证等RFC2617及以外的其它认证算法。同时,一个Webrick服务器还能由多个Webrick服务器或服务器小程序组合,提供类似虚拟主机或路由等功能:例如处理CGI脚本、ERb页面、Ruby块以及目录服务等。

Webrick曾被用于Rails核心团队的开发和测试中。但是,Webrick内置的HTTP Parser非常古老,文档缺失,性能低下且不易维护,功能单一且默认只支持单进程模式(但支持多线程,不过在Rails中默认关闭了对Webrick的多线程支持),根本无法满足产品环境中的并发和日常维护需求。目前一般只用于Web应用的本地开发和基准测试。

fcgi.rb

fcgi.rb是FastCGI协议的Ruby封装(latest版底层依赖libfcgi)。为了与当时的NSAPI竞争,FastCGI协议最初由Open Market提出和开发、并应用于自家Web服务器,延续了前者采用独立进程处理请求的做法:即维持一个FastCGI服务器。当Web服务器接收到HTTP请求时,请求内容和环境信息被通过Socket(本地)或TCP连接(远程)的方式传递至FastCGI服务器进行处理,再通过相反路径返回响应信息。分离进程的好处是Web服务器进程和FastCGI进程是永远保持的,只有相互之间的连接会被断开,避免了进程管理的开销。

Web服务器和FastCGI:SCGI服务器

Web服务器和FastCGI/SCGI服务器

进一步,FastCGI还支持同时响应多个请求。为了尽量减少资源浪费,若干请求可以复用同一个与Web服务器之间的连接,且支持扩展至多个FastCGI服务器进程。FastCGI降低了Web服务器和应用程序之间的耦合度,进而为解决安全、性能、管理等各方面问题提供新的思路,相比一些嵌入式方案如mod_perl和mod_php更具灵活性。

由于FastCGI协议的开放性,主流Web服务器产品基本都实现了各自的FastCGI插件,从而导致FastCGI方案被广泛使用。fcgi.rb最早开发于1998年,底层包含C和Ruby两种实现方式,早期曾被广泛应用于Rails应用的产品环境。

mod_ruby

mod_ruby是专门针对Apache HTTP Server的Ruby扩展插件,支持在Web服务器中直接运行Ruby CGI代码。由于mod_ruby在多个Apache进程中只能共享同一个Ruby解释器,意味着当同时运行多个Web应用(如Rails)时会发生冲突,存在安全隐患。因此只在一些简单部署环境下被采用,实际上并没有普及。

LiteSpeed API/RubyRunner

LiteSpeed是由LiteSpeed Tech公司最初于2002年发布的商用Web服务器,特点是与被广泛采用的Apache Web服务器的配置文件兼容,但因为采用了事件驱动架构而具有更好的性能。

LiteSpeed API(LSAPI)是LiteSpeed专有的服务器API,LSAPI具备深度优化的IPC协议以提升通信性能。类似其它Web服务器,LiteSpeed支持运行CGI、FastCGI、以及后来的Mongrel。同时在LSAPI的基础上开发了Ruby接口模块,支持运行基于Ruby的Web应用。此外,LiteSpeed还提供RubyRunner插件,允许采用第三方Ruby解释器运行Ruby应用,但综合性能不如直接基于LSAPI Ruby。

由于LiteSpeed是收费产品,其普及率并不高,一般会考虑采用LiteSpeed作为Web服务器的业务场景包括虚拟主机/VPS提供商、以及相关业务的cPanel产品。同时,LiteSpeed也会被用于一些业务需求比较特殊的场合,例如对Web服务器性能要求高,且应用程序及其部署需要兼容Apache服务器。LiteSpeed于2013年发布了开源的轻量Web服务器——OpenLiteSpeed(GPL v3),移除了商业版本中偏具体业务的功能如cPanel等,更倾向于成为通用Web服务器。

scgi.rb

scgi.rb是对SCGI协议的纯Ruby实现。从原理上来看,SCGI和FastCGI类似,二者的性能并无多大差别。但比起后者复杂的协议内容来说,SCGI移除了许多非必要的功能,看起来十分简洁,且实现复杂度更低。

Web服务器和多FastCGI:SCGI服务器

Web服务器和多FastCGI/SCGI服务器

与FastCGI类似,一个SCGI服务器可以动态创建服务器子进程用于处理更多请求(处理完毕将转入睡眠),直至达到配置的子进程上限。当获得Web服务器请求时,SCGI服务器进程会将其转发至子进程,并由子进程运行CGI程序处理该请求。此外,SCGI还能自动销毁退出和崩溃的子进程,具有良好的稳定性。

二、闻名天下

2005年,David Heinemeier Hansson(DHH)发布了基于Ruby的开发框架Ruby on Rails(Rails),聚光灯第一次聚焦在Ruby身上。但是业内普遍对Web服务器的方案感到棘手,本地环境Webrick/产品环境FastCGI+通用Web服务器几乎成了标配,无论是开发、部署或维护都遇到不少困难,一些吃螃蟹的人遂把此视为Rails不如J2EE、PHP方案的证据。

Mongrel

2006年,Zed Shaw发布了划时代的Mongrel。Mongrel把自己定位成一个“应用服务器”,因为其不仅可以运行Ruby Web应用,也提供标准的HTTP接口,从而使Mongrel可以被放置在Web代理、Load Balancer等任意类型的转发器后面,而非像FastCGI、SCGI一样通过调用脚本实现Web服务器和CGI程序的通信。

Mongrel采用Ragel开发HTTP/1.1协议的Ruby parser,而后者是一个高性能有限自动机编译器,支持开发协议/数据parser、词法分析器和用户输入验证,支持编译成多种主流语言(包括Ruby)。采用Regel也使parser具有更好的可移植性。但是,Mongrel本身不支持任何应用程序框架,而需要由框架自身提供这种支持。

Mongrel Web服务器

Mongrel Web服务器

Mongrel支持多线程运行(但对于当时非线程安全的Rails来说,仍然只能采用多进程的方式提高一部分并发能力),曾被Twitter作为其第一代Web服务器,还启发了Ryan Dahl发布于2009年的Node.JS。

但是当Mongrel发布后没过多久,Shaw就与Rails社区的核心成员不和(实际上Shaw对业界的许多技术和公司都表达过不满),随后就终止了Mongrel的开发。进而在其Parser的基础上开发了其后续——语言无关的Web服务器Mongrel2(与前续毫无关系)。

尽管Mongrel迅速衰落,却成功启发了随后更多优秀Ruby应用服务器的诞生,例如后文将介绍的Thin、Unicorn和Puma。

Rack

随着Web服务器接口技术的发展,从开始时作为一个module嵌入Web服务器,到维护独立的应用服务器进程,越来越多的应用服务器产品开始涌现,同时相互之间还产生了差异化以便适应不同的应用场景。但是,由于底层协议和API的差别,基于不同的应用服务器开发Web产品时,意味着要实现各自的通信接口,从而为Web应用开发带来更多工作量。特别是对于类似Django、Rails这些被广泛使用的Web框架来说,兼容主流应用服务器几乎是必须的。

2003年,Python界权威Phillip J. Eby发表了PEP 0333(Python Web Server Gateway Interface v1.0,即WSGI),提出一种Web服务器和应用程序之间的统一接口,该接口封装了包括CGI、FastCGI、mod_python等主流方案的API,使遵循WSGI的Python Web应用能够直接部署在各类Web服务器上。与Python的发展轨迹相似,Ruby界也遇到了类似的挑战,并最终在2007年出现了与WSGI类似的Rack。

与WSGI最初只作为一份建议不同,Rack直接提供了模块化的框架实现,并由于良好的设计架构迅速统一了Ruby Web服务器和应用程序框架接口。

Rack被设计成一种中间件“框架”,接收到的HTTP请求会被rack放入不同的管线(中间件)进行处理,直到从应用程序获取响应。这种设计通过统一接口,把一般Web应用所需的底层依赖,包括Session处理、数据库操作、请求处理、渲染视图、路由/调度、以及表单处理等组件以中间件的形式“放入”rack的中间件管线中,并在HTTP请求/响应发生时依次通过上述管线传递至应用程序,从而实现Web应用程序对底层通信依赖的解绑。

Rack中间件

Rack中间件

Rack接口部分包含两类组件:Handler,用于和Web服务器通信;Adapter,用于和应用程序通信。截至Rack 1.6,Rack内置的handlers包括WEBrick、FCGI、CGI、SCGI、LiteSpeed以及Thin,上述handlers用以兼容已有的常见应用服务器。而2008年后,随着rack逐渐成为事实标准,更新的Ruby Web服务器几乎都包含Rack提供的handler。包括Rails、Sinatra、Merb等等几乎所有主流框架都引入了Rack Adapters的支持。

三、百花齐放

Mongrel和Rack的相继诞生,使Ruby Web服务器、乃至应用程序框架的发展有了一定意义上可以遵循的标准。Mongrel后相继派生出Thin、Unicorn和Puma;而Rack统一了Ruby Web服务器和应用程序框架接口,使应用开发不再需要考虑特定的部署平台。Ruby Web服务器开始依据特定需求深入发展。

Thin/Goliath

发布于2009年的Thin沿用了Mongrel的Parser,基于Rack和EventMachine开发,前者上文已有介绍,EventMachine是一个Ruby编写的、基于Reactor模式的轻量级事件驱动I/O(类似JBoss Netty、Apache MINA、Python Twisted、Node.js、libevent和libev等)和并发库,使Thin能够在面对慢客户端的同时支持高并发请求。

发表自1995年的Reactor模型的基本原理是采用一个单线程事件循环缓存所有系统事件,当事件发生时,以同步方式将该事件发送至处理模块,处理完成后返回结果。基于Reactor模型的EventMachine具备异步(非阻塞)I/O的能力,被广泛用于大部分基于Ruby的事件驱动服务器、异步客户端、网络代理以及监控工具中。

Reactor模型

Reactor模型

2011年,社交网络分析商PostRank开源了其Web服务器Goliath,与Thin相似(都采用了EventMachine)但又有很大不同,采用新的HTTP Parser,同时针对异步事件编程中的高复杂度回调函数问题,借助Ruby1.9+的纤程技术实现了线性编码,使程序具备更好的可维护性。Goliath支持MRI、JRuby和Rubinius等多平台。在附加功能方面,Goliath的目标不仅是作为Web服务器,更是一个快速构建WebServices/APIs的开发框架,但是随着之后PostRank被Google收购,Goliath项目也就不再活跃在开源界了。

Unicorn

2009年,Eric Wong在Mongrel 1.1.5版本的基础上开发了Unicorn。Unicorn是一个基于Unix/类Unix操作系统的、面向快客户端、低延迟和高带宽场景的Rack服务器,基于上述限制,任何情况下几乎都需要在Unicorn和客户端之间设置一个反向代理缓存请求和响应数据,这是Unicorn的设计特点所决定的,但也使得Unicorn的内部实现相对简洁、可靠。

尽管来源于Mongrel,但Unicorn只在进程级运行,且吸收和利用了一些Unix/类Unix系统内核的特性,如Prefork模型。

Unicorn由1个master进程和n个fork(2)子进程组成,子进程分别调用select(2)阻塞自己,直到出错或者超时时,才做一些写日志、处理信号以及维护与master的心跳链接等内置任务。子进程和master间通过一个共享socket实现通信,而由Unix/类Unix系统内核自身处理资源调度。

Unicorn的多进程模型

Unicorn的多进程模型

Unicorn的设计理念是“只专注一件事”:多进程阻塞I/O的方式令其无从接受慢客户端——但前置反向代理能解决这一问题;workers的负载均衡就直接交给操作系统处理。这种理念大大降低了实现复杂度,从而提高了自身可靠性。此外,类似Nginx的重加载机制,Unicorn也支持零宕机重新加载配置文件,使其允许在线部署Web应用而不用产生离线成本。

Phusion Passenger(mod_rails/mod_rack)

2008年初,一位叫赖洪礼的Ruby神童发布了mod_rails。尽管Mongrel在当时已经席卷Rails的Web服务器市场,但是面对部署共享主机或是集群的情况时还是缺少统一有效的解决方案,引起业内一些抱怨,包括DHH(也许Shaw就不认为这是个事儿)。

mod_rails最初被设计成一个Apache的module,与FastCGI的原理类似,但设置起来异常简单——只需要设置一个RailsBaseURI匹配转发至Rails服务器的URI串。mod_rails服务器会在启动时自动加载Web应用程序,然后按需创建子进程,并协调Web服务器和Rails服务器的通信,从而支持单一服务器同时部署多个应用,还允许按需自动重启应用服务器。

mod_rails遵循了Rails的设计原则,包括Convention over Configuration、Don’t Repeat Yourself,使其面向部署非常友好,很快得到了业界青睐,并在正式release时改名Passenger。

在随后的发展中,Passenger逐渐成为独立的Ruby应用服务器、支持多平台的Web服务器。截至2015年6月,Phusion Passenger的版本号已经达到5.0.10(Raptor),核心采用C++编写,同时支持Ruby、Python和Node.js应用。支持Apache、Nginx和独立HTTP模式(推荐采用独立模式),支持Unix/类Unix系统,在统计网站Builtwith上排名Ruby Web服务器使用率第一。

值得一提的是,Phusion Passenger的开源版本支持多进程模式,但是其企业版同样支持多线程运行。本文撰写时,Phusion Passenger是最近一个号称“史上最快”的Ruby Web服务器(本文最后将进一步介绍Raptor)。

Trinidad/TorqueBox

Trinidad发布于2009年,基于JRuby::Rack和Apache Tomcat,使Rails的部署和世界上最流行的Web服务器之一Tomcat结合,支持集成Java代码、支持多线程的Resque和Delayed::Job等Worker,也支持除Tomcat以外的其它Servlet容器。

与Trinidad相比,同样发布于2009年的TorqueBox不仅仅是一个Web服务器,而且被设计成一个可移植的Ruby平台。基于JRuby::Rack和WildFly(JBoss AS),支持多线程阻塞I/O,内置对消息、调度、缓存和后台进程的支持。同时具有集群、负载均衡、高可用等多种附加功能。

Puma

Puma——Mongrel最年轻的后代于2011年发布,作者是Evan Phoenix。

由于Mongrel诞生于前Rack时期,而随着Rack统一了Web服务器接口,任何基于Rack的应用再与Mongrel配合就有许多不便。Puma继承了前者的Parser,并且基于Rack重写了底层通信部分。更重要的是,Puma部分依赖Ruby的其它两个流行实现:Rubinius和JRuby,与TorqueBox类似拥有多线程阻塞I/O的能力(MRI平台不支持真正意义上的多线程,但Puma依然具有良好并发能力),支持高并发。同时Puma还包含了一个事件I/O模块以缓冲HTTP请求,以降低慢客户端的影响。但是,从获得更高吞吐量的角度来说,Puma目前仍然需要采用Rubinius和JRuby这两个平台。

Reel

Reel是最初由Tony Arcieri发布于2012年的采用事件I/O的Web服务器。采用了不同于Eventmachine的Celluloid::IO,后者基于Celluloid——Actor并发模型的Ruby实现库,解决了EM只能在单一线程中运行事件循环程序的问题,从而同时支持多线程+事件I/O,在非阻塞I/O和多线程方案间实现了良好的融合。

与其它现代Ruby Web服务器不同的是,Reel并不是基于Rack创建,但通过Reel::Rack提供支持Rack的Adapter。尽管支持Rails,与Puma也有一定的相似性,但与Unicorn、Puma和Raptor相比,Reel在部署Rails/Rack应用方面缺少易用性。实际上基于Celluloid本身的普及程度和擅长领域,相比其它Web服务器而言,Reel更适合部署WebSocket/Stream应用。

Yahns

2013年,Eric Wong等人受Kqueue(源自FreeBSD,同时被Node.js作为基础事件I/O库)的启发启动了Yahns项目。其目标与Reel类似,同样是在非阻塞I/O设计中引入多线程。与Reel不同的是,Yahns原生支持Rack/HTTP应用。

Yahns被设计成具有良好的伸缩性和轻量化特性,当系统应用访问量较低或为零时,Yahns本身的资源消耗也会保持在较低水平。此外,yahns只支持GNU/Linux(并通过kqueue支持FreeBSD),并声称永远不会支持类似Unicorn或Passenger里的Watchdog技术,不会因为应用崩溃而自动销毁和创建进程/线程,因此对应用程序本身的可靠性有一定要求。

四、迈向未来

回顾过去,Ruby Web服务器在发展中先后解决了缺少部署方案、与Web应用程序不兼容、运维管理困难等问题,基础架构趋于成熟且稳定。而随着更多基准测试结果的出现,业界逐渐开始朝着更高性能和并发量发展,同时针对HTTP协议本身的优化和扩展引入的HTTP/2,以及HTML5的WebSocket/Stream等需求均成为未来Ruby Web服务器发展的方向。

高吞吐量

以最新的Raptor(上文提到的Phusion Passenger 5)为例,其在网络I/O模型的选择上融合了现有其它优秀产品的方案,包括Unicorn的多进程模型、内置基于多线程和事件I/O模型的反向代理缓冲(类似Nginx的功能,但对Raptor自身做了大量裁减和优化)、以及企业版具有的多线程模型(类似Puma和TorqueBox);此外,Raptor采用的Node.js HTTP Parser(基于Nginx的Parser)的性能超过了Mongrel;更进一步,Raptor甚至实现了Zero-copy和一般在大型游戏中使用的区域内存管理技术,使其对CPU和内存访问的优化达到了极致(感兴趣的话可以进一步查阅这里)。

Raptor的优化模型

Raptor的优化模型

另外也需要看到,当引入多线程运行方式,现有Web应用将不得不仔细检查自身及其依赖,是否是线程安全的,同时这也给构建Ruby Web应用带来更多新的挑战。这也是为什么更多人宁愿选择进程级应用服务器的方式——毕竟对大多数应用来说不需要用到太多横向扩展,引入反向代理即可解决慢客户端的问题,而采用Raptor甚至在独立模式能工作的更好(这样就不用花时间去学习Nginx)。

除非你已经开始考虑向支持大规模并发的架构迁移,并希望节省接下来的一大笔花费了。

HTTP/2

2015年5月,HTTP/2随着RFC7540正式发布。如今各主流服务器/浏览器厂商正在逐渐完成从HTTP/2测试模块到正式版本的过渡。而截至目前,主流Ruby Web服务器都还没有公开HTTP/2的开发信息。HTTP-2是在2013年由Ilya Grigorik发布的纯Ruby的HTTP/2协议实现,包括二进制帧的解析与编码、流传输的多路复用和优先级制定、连接和流传输的流量控制、报头压缩与服务器推送、连接和流传输管理等功能。随着HTTP/2的发布和普及,主流Ruby Web服务器将不可避免的引入对HTTP/2的支持。

WebSocket/流(Stream)/服务器推送事件(Server Sent Events,SSE)

2011年,RFC6455正式公布了WebSocket协议。WebSocket用于在一个TCP链接上实现全双工通信,其目的是实现客户端与服务器之间更高频次的交互,以完成实时互动业务。鉴于该特点,仅支持慢客户端的Web服务器就无法有效支撑WebSocket的并发需求,更何况后者对并发量更加严苛的要求了。而对于同样需要长连接的流服务器和服务器推送事件服务(SSE),都避免不了对长连接和高并发量的需求。尽管高性能的Ruby Web服务器都有足够的潜力完成这些任务,但是从原生设计的角度来看,更加年轻的Reel和Yahns无疑具有优势。

最近Planet ruby在Ruby邮件组发布了Awesome Webservers,该Github Repo旨在对主流Ruby Web服务器进行总结和对比,并且保持持续更新,推荐开发者关注。

Share