• Feeds

  • 软件领域的Reinvent

    苹果经常喜欢用“Reinvent”这个词,用在硬件领域确实高大上。但在软件领域Reinvent大多时候是个贬义词,软件领域上下游结合及相互依赖非常紧密,没有哪个团队甚至公司能封闭的完成所有事情。因此重新发明这件事情在软件领域并不吃香,一个新的体系在小的团队通常会自生自灭(尽管途中可以获得一些荣誉),剩下那些硬撑的项目则会让参与人员痛苦不堪。

    比如各厂定制的各种百花齐放的框架,运作几年之后,核心系统越来越臃肿,随着人员的更替能深入了解内部的人也越来越少,改进的动作非常迟钝。但大量的上下游业务服务又需要依赖这些系统,新来的人需要较高的成本熟悉整套系统。问题丛生,但是离开又需要付出极大代价,于是就成了一个鸡肋。如果使用方牵涉到多个部门,更不会有人站起来说将这些定制系统废弃或迁移。

    但这个现象在计算机语言领域则是一个例外。一门新的语言,不喜欢的人可以不用,而且大部分开发团队是随大流,不会选用小众语言,所以新语言领域已经在搭建的哪些不成熟的系统也不会给业界带来麻烦及痛苦。少量情况下,极客会心爱的小众语言搭建的一些系统,但根据观察,这些系统通常会随着极客的离职马上会被用主流语言重写替换。因此在新语言领域,可以用更轻松的心态去重新发明各种组件。

    一门语言出来之后,复用另外一种语言的模块与组件不是最佳方案,理想情况会选择构建自己的函数库,框架,开发工具,测试工具,代码工具,性能调优工具等。在新的语言出来的早期,这些领域一片空白,踏入这片领域的人会有新大陆淘金的感觉,随便写点东西,很快可以找到市场。

    在一个新的语言重写一些别的语言已经完成的项目,尽管这不是什么伟大的发明创造,但参与的人往往更快的取得了成绩并且赢得了尊敬(相对于在“主流”语言拼搏那些人)。我也曾经思考过这个现象。技术人的价值观虽然很多元化,但是有两点却几乎是共通的,一是工程师动手创造带来的成就感,二是感受创造的东西给所在环境以及业界带来使用价值的成就感,不管这些创造是原创的还是山寨。因此在很多领域如搜索、广告以及更基础层面OS、Database、Language总是有乐此不疲工程师重新去建设,因此在语言层面的重新建设就变得容易理解了。

    更深层次的来看,在语言层面的基础上,重新去构建一个生态圈,从社区的角度,不只是重新克隆一个新的版本,工程师会考虑以往的经验及教训,重新设计重要的单元,以带来性能上的提升及使用方式上的改进。比如这几天写一个Go语言的小程序,看到Go的社区在配置文件方面的思考及重新选择。配置管理在Java领域大多是XML一统天下,尽管其可维护性较差,但由于历史原因,工程师很难去另起炉灶。

    当然在新OS领域重新创造的机会就更多,但是这种情况较少发生,一个新的OS生命周期会延续10年或更久。一些大公司的开发平台生态圈也有类似的这样的机会,不过大部分情况下格局会小很多,也会存在更大的不确定性。

    团队的技术形象

    当一个app不好用时候,用户会说团队的产品不行;当系统可用性有问题时(比如访问慢或频繁宕机),用户会说这个团队技术不行。

    发生频繁宕机并不常见,宕机通常会由程序bug或者访问压力过大引起。bug最终会被修复;在不过分在意成本的情况下,访问压力最终可以用硬件来扛住。另外系统升级及功能变更也会带来可用性与稳定性的问题,但大多数系统会最终相对稳定下来,大的变更会越来越少,系统也会趋于稳定。

    技术成本的控制通常不是一个很紧迫的问题,一方面公司的业务模式也不是靠省服务器做出来的,其次技术团队可以通过投入时间去优化改善来降低成本(但大部分团队更愿意将研发资源投入到新功能开发上去)。因而通常只要达到基本可用,则成本合理性问题不会得到充分关注。另外成本也很难直接对比,一个公司比另外一个公司服务器少些,但是各自功能场景有差异,没有直接可比性。因此较难从技术成本合理性角度衡量某个团队技术好不好。讽刺的大部分现实是,只要系统基本可用,则会被视为一支有战斗力的团队;如果能达到一定体量(虽然不是技术团队直接带来的),那则会被视为超强的技术团队。

    软件质量在大部分公司走不出测试或者“质量保证”部门,很少有团队会认为步子不够快是由于软件质量问题造成。开发效率同样没有直接可比性,“努力”有时候会掩盖一切,朋友圈时常会看到这样的总结,“虽然走了一些弯路,但兄弟们加班加点将进度追了回来,感动……”。比软件质量问题更糟糕的是业务方向的不确定性,方向不确定带来的消耗比软件质量的问题更大,走错方向导致一两个月努力泡汤的事情也很常见,因此不是最短板的软件质量的重视通常就无从提起了。

    隔洋对岸则有一些技术主导的公司,Mark Zuckerberg曾说过,Facebook在早期时,非技术岗位如HR及市场也招聘有技术背景的人担任。这些公司尽量招聘最顶级的技术人才,这些人能够在公司中享受技术的乐趣。但这也只有平台级的公司才有可能。这些公司商业模式的门槛足够高,从而有持续的利润来支持这一愿景。而剩下绝大多数公司,需要首先去考虑生存的问题,这也很正常,头部的公司如果失去垄断地位,也马上没有机会去维持纯技术形象。

    技术行不行是一个很无力的目标,技术很强的团队未必能改变世界,不强的团队很多时候也生存得很好。业界很多业务模式短板并不在技术上,更混乱的是,出现技术问题有时反而被当做PR事件及段子去炒作。当然,在技术较差的团队,软件质量及技术创新无从谈起。处在风口的猪,也会遇到前文所说的重复踩进同一个坑的无力感。

    为什么超长列表数据的翻页技术实现复杂(二)

    上文为什么超长列表数据的翻页技术实现复杂提到了超长列表翻页技术设计上一些问题,今天讨论下部分解决思路。

    前新浪同事 @pi1ot 最近在程序员杂志发表的一篇文章《门户级UGC系统的技术进化路线》也是超长列表的一个经典案例,在正式展开思路之前,我们也不妨了解一下此文所说新浪评论系统的演进思路。

    从文中看到几个版本的列表翻页实现方案

    3.0版

    3.0系统的缓存模块设计的比较巧妙,以显示页面为单位缓存数据,因为评论页面是依照提交时间降序排列,每新增一条新评论,所有帖子都需要向下移动一位,所以缓存格式设计为每两页数据一个文件,前后相邻的两个文件有一页的数据重复,最新的缓存文件通常情况下不满两页数据。

    此方案由于每页的条数是定长的,因此主要采用缓存所有列表的方案。但为了数据更新的便利,缓存结构比较复杂。从今天多年之后的眼光来看,这种设计不利于理解、扩展及维护。因此目前大多不倾向使用这种方案。

    4.0版

    解决方案是在MySQL数据库和页面缓存模块之间,新建一个带索引的数据文件层,每条新闻的所有评论都单独保存在一个索引文件和一个数据文件中,期望通过把对数据库单一表文件的读写操作,分解为文件系统上互不干涉可并发执行的读写操作,来提高系统并发处理能力。在新的索引数据模块中,查询评论总数、追加评论、更新评论状态都是针对性优化过的高效率操作。从这时候起,MySQL数据库就降到了只提供归档备份和内部管理查询的角色,不再直接承载任何用户更新和查询请求了。

    使用自定义索引的方式,由于未与相关人员交流细节,推断应该是类似数组的结构。

    从上述案例看到,评论系统是一种典型的超长列表数据结构,如果再MySQL的基础上来做,需要设计额外的索引结构来实现高效的翻页功能

    由于超长列表的翻页实现成本高主要是由于列表索引的B-TREE结构方面原因,B-TREE结构能快速查找到某个key,但不是为叶子节点的Range访问而设计,因此主要解决思路也是围绕B-TREE的range访问而进行优化。

    首先、从原理来看,可以在B-TREE增加以下2个二级索引字段:

    • Count index 记录每个非叶子节点下的条目数,这样可以帮助快速定位到任意的offset;
    • Offset index 记录部分叶子节点的offset,比如每隔1000个id记录一个offset如下,并保存在另外一个列表中,当需要查找某个offset的时候,则可以利用附近已经记录offset的id来定位目的地位置。比如当翻页到1010时,如果offset index记录了[1000: id10345],则可以从id10345往后10个元素找到10010。
    [
    {"1000":10345},
    {"2000":13456},
    {"3000":22345},
    {"4000":56789},
    {"5000":66788}
    ]
    

    这2个字段可以同时使用,也可以只用其中一个。如图
    btree-index

    再看如何将上述方法应用到具体的实现中。由于本文主要讨论MySQL环境,MySQL要在B-TREE上额外保存一些信息需要修改MySQL源代码,修改门槛较高,因此更简单方法是将上述二级索引通过应用层保存在另外的表中。

    一种非严格意义的count index实现如下:

    CREATE TABLE IF NOT EXISTS `second_index` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `uid` int(11) NOT NULL,
      `yymm` int(11) NOT NULL,
      `index_count` int(11) NOT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB  DEFAULT CHARSET=utf8;
    
    
    INSERT INTO `second_index` (`id`, `uid`, `yymm`, `index_count`) VALUES
    (1, 1, 1409, 123),
    (2, 1, 1410, 2342),
    (3, 1, 1411, 534),
    (4, 1, 1412, 784),
    (5, 1, 1501, 845);
    

    一种offset index实现如下:
    在第一次用户翻页到某个offset位置时,在redis直接保存offset, id。当有其他请求来查找offset之后数据时,可以从offset的位置往后扫描。如果列表的数据发生了变化,需要及时将Redis保存的offset index删除。

    以上2种方法已经在生产环境使用。

    @icycrystal4对此文所提方法亦有贡献。