BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

独家揭秘腾讯千亿级参数分布式机器学习系统无量

| 作者 袁镱 关注 1 他的粉丝 发布于 2018年7月4日. 估计阅读时间: 30 分钟 | ArchSummit北京2018 共同探讨机器学习、信息安全、微服务治理的关键点

简介

在互联网场景中,亿级的用户每天产生着百亿规模的用户数据,形成了超大规模的训练样本。如何利用这些数据训练出更好的模型并用这些模型为用户服务,给机器学习平台带来了巨大的挑战。下面以网页/图文/视频推荐场景分析这些挑战,下文中均称为推荐场景。

  1. 样本数量大。在推荐场景下,每天的样本量可以达到百亿量级。如果需要按一个月的样本进行训练,样本量会在千亿级别。如果每个样本平均500特征值,单个样本的大小就是5KB左右,一千亿样本的大小就是500TB。即便只使用一周内的样本,样本数据的大小也在100TB这个级别。
  2. 特征维度多。巨大的样本量使高维度模型的训练成为可能。为了给用户提供更合理的推荐结果,需要对用户和被推荐的文章/图片/视频进行详细的描述。各个业务都会建立起丰富的用户模型,也会对文章/图片/视频进行多维度的标注。在系统进行推荐时,还会使用到用户现在的上下文信息,比如:时间,位置,之前浏览的页面等。当这些特征被引入到模型中时,会导致特征规模的迅速增加。如果再考虑交叉等特征转换操作,模型的特征维度会轻松地膨胀到千亿甚至万亿级别。
  3. 训练性能要求高。我们面对的是百TB的样本和百亿/千亿参数的模型。而业务需要在短时间内训练出一个性能指标好的模型,以便快速上线验证。这对机器学习平台训练能力有很高的要求。
    前面1~3点,提出的是超大规模模型的训练框架面临的挑战,然而训练出模型只是重要的第一步。最终模型需要上线为用户提供服务才能体现其业务价值。对于以往的机器学习中的中小模型,模型上线服务并不是一个特别被关注的事情。但是,当最终模型文件很大,甚至超过单机的内存大小时,模型上线服务就变成了棘手的问题。
  4. 模型大但用户需要毫秒级响应。以最简单的LR模型为例,一个10亿特征的模型的大小也会达到12GB(每个参数需要一个8Byte的key和4Byte的float value)。如果是DNN模型,模型大小到达TB也是可能的。当训练好一个模型后,模型就被上线,为用户提供预测服务。为了达到良好的用户体验,预测服务的响应时间需要在10ms这个量级。以手机用户的推荐场景为例,从用户在手机上刷新页面到看到推荐结果,时间不能超过1s,扣除掉网络通讯的开销,IDC内在线服务的响应时间需要控制在200ms以内。但是,整个推荐的流程至少有召回,排序和展示控制三个阶段。在排序阶段,需要对200个以上的文章进行特征拼接和点击率预估,所以模型对这200个文章进行点击率预估的总时间要在30ms以内。如何使用这么大规模的模型进行高性能,高并发的预测也对平台能力的重大考验。
  5. 模型实时上线。对于资讯推荐类场景,用户的关注点变化很快。系统需要根据最新的用户行为数据调整模型,然后以最快的速度将如此大规模的模型更新到多个地区的在线预测服务。

为了解决以上挑战,我们:

1)开发了一个基于参数服务器架构的机器学习计算框架--无量框架,已经能够完成百亿样本/百亿参数模型的小时级训练能力。无量框架提供多种机器学习算法,不但能进行任务式的离线训练,还能支持以流式样本为输入的7*24小时的在线训练。

2)在无量框架的基础上,我们构建起自动化模型管理系统--无量模型管理,模型能够在离线训练任务,在线训练集群和在线预测服务之间无缝地高效流转,实现10GB级模型的分钟级上线。

3)为了提高大模型的线上预测服务,我们还开发了高性能的预测模块和服务--无量预测服务,对于数十GB的模型,只需几毫秒即可完成100个样本的预测。

无量框架,无量模型管理和无量预测服务共同构成了无量系统的主要部分。下面我们将对无量系统的架构和各个主要组成部分进行详细的介绍。

1. 系统流程与架构

工作流程

在广告/推荐等场景下,模型的生产和使用过程,大致分为几个步骤:

  1. 日志采集与样本生成。通过收集用户的线上行为信息,生成模型需要的样本。这些样本可以被存储起来用于离线训练,也可以使用流式数据的方式推送给在线训练集群。
  2. 模型训练。有了样本之后,在训练集群中训练出具体的模型。开发人员通过调整的超参数或模型结构来获取好的模型。
  3. 模型评估。在模型被放到线上服务之前,需要对模型进行一些评估工作。
  4. 模型上线预测。

无量系统目前包括以上步骤中的模型训练,模型评估和上线预测。

为了让模型从训练集群到在线预测服务顺利地流转,无量系统提供了模型管理功能,能够自动化地将从训练机器导出新模型到在线预测服务。业务也能够在模型自动化上线过程中定义模型评估操作,避免训练效果差的模型被放到在线预测服务中。另外当模型上线之后,也需要验证线上的模型是否有问题。

在模型的开发过程中,超参数调试耗费了模型开发人员大量的时间。无量系统通过与般若系统结合,实现了模型训练效果的实时监控,为自动化调参提供了决策数据。无量系统正在进行自动调参工具的开发。算法人员也可以基于这些数据上实现自定义自动调参功能。

系统架构

在一个机器学习系统中,机器学习算法的代码只是训练或预测过程中非常小的一部分。以下是Google对其机器学习系统的一个统计。

[1 ] Hidden Technical Debt in Machine Learning Systems, Google.

为了让机器学习任务运行起来,需要大量的配套服务设施的支持,包括数据管理与存储,训练框架,在线预测框架和资源管理等多个模块。无量系统的系统架构如下所示:

系统架构图

无量系统的训练任务运行在MIG专门针对机器学习和深度学习任务的般若调度系统上,在线训练集群和在线预测服务部署在Sumeru系统上。般若系统和Sumeru系统均是基于docker容器化技术构建,为无量系统的快速部署和扩展提供了可靠的基础设施保障。

下层存储系统支持HDFS和ceph两种分布式网络存储。HDFS作为常用的分布式网络存储,与其他的数据分析系统无缝对接。Ceph以其高性能与灵活的文件操作,弥补了Hdfs在文件操作上的不便。

日志采集使用MIG的灯塔系统,并配合了自研的流式数据处理服务实时生成训练样本。

通过自研的基于参数服务器架构的无量计算框架,无量系统支持了千亿级模型的任务型离线训练和流式在线训练。无量计算框架采用C++实现以达到优异的性能,并支持了推荐和搜索场景常用的LR/FM/FFM/DNN等模型,用户只需做简单的配置即可实现超大规模模型的训练。

对于千亿级参数的模型,模型大小至少会有几十GB。无量系统为业务的在线预测服务提供了两种模型使用模式:1)模型服务组件。模型服务组件包含了模型版本管理和模型预测两个主要功能。由于模型服务组件对内存管理进行深度优化,业务能够在自己的预测服务中直接加载和使用100G以下的模型。2)模型存储服务。当模型大小超过单机能够存放的大小时,就需要分布式的模型存储服务来进行模型管理和提供预测服务。

在样本生成,模型训练,在线预测模块之上,是无量系统的平台服务。用户在这里管理自己的数据,训练任务和模型。

至此,我们简单介绍了无量系统的各个部分。让读者能够对无量系统有一个整体的了解。下面,我们将重点介绍无量计算框架,模型管理与模型预测服务。

2. 无量计算框架

为了得到一个好的模型,至少需要三个方面的内容:1.数据;2.模型和算法;3.计算框架。正如前面介绍中所述,互联网用户为我们产生了大量的样本数据,为学习出一个好的模型提供了数据基础。在本节中,我们将重点介绍后面两部分内容。首先我们会介绍推荐场景常用的模型和算法,并由此推导出为了实现这些模型的训练,对计算框架有什么样的需求,以及无量计算框架如何满足这些需求,实现高性能的模型训练。

推荐模型与算法

随着商业化推荐的兴起,预测用户点击率(Click Through Rate,简称 CTR)领域得到了大量的研究关注,产生了很多CTR预估模型。下面对大规模场景下的几个代表性的模型做简单的对比介绍。他们分别是LR,FM,DNN。对于推荐场景中常用的GBDT算法,由于其不适应大规模特征的输入,在此不做对比。

LR模型

LR是一个简单而有用的线性模型。优点:它实现简单而且非常容易支持大规模特征的样本输入。在实际应用中,往往能取得不错的效果,常常被用作baseline。缺点:由于是线性模型,需要大量的特征工程的工作来让它得到好的效果。而特征交叉等操作也直接导致了模型的特征空间急剧膨胀。

FM模型

FM在LR线性模型的基础上,引入了二次项,使得FM模型能够自动学习特征之间的二阶交叉关系。优点:自动学习二阶交叉关系,减少了部分特征工程的工作。缺点:要学习二阶以上的交叉关系,仍然需要进行交叉特征选择的工作来生成相应的样本。

DNN模型

随着深度神经网络(DNN)在图像、语音等领域的突破性发展,DNN被引入到CTR模型中来,希望学习到特征之间的复杂关系,得到更好的模型。在CTR预估中,输入特征是高维稀疏的,不能直接使用全连接网络直接进行学习,所以用于CTR预估的网络一般采用embedding层+全连接层的结构。通过embedding层将稀疏特征转换为低维稠密特征,再输入后面的全连接层。优点:可以直接输入原始特征,减少了交叉特征的选择工作。缺点:训练调参相比LR和FM等更难。由于DNN稠密参数的引入,训练性能也比LR和FM更低。

[Google 2016] Wide & Deep Learning for Recommender Systems

前面简单介绍了三种代表性的模型,在这三种基本结构上,通过不同的组合和改进,衍生出了FFM,FNN,DeepFM,DIN等模型结构。如果想详细了解相关的模型,请见参考文献[3][4]。

从上面的模型基本结构,我们可以总结出CTR模型的参数特点:

1)超大规模稀疏的输入特征参数。LR,FM和DNN的embedding层的输入都是稀疏的,参数值可能是一个单独的值(LR的w),也有可能是一个向量(FM中的w+v和embedding层的w)。

2)稠密的交叉关系参数。DNN中全连接层参数都是稠密的。

由此可以看出,计算框架需要同时支持稀疏和稠密两种参数格式。另外,一些统计类特征(例如:文章的曝光数,点击率等)在训练中也是很重要的。这些参数也需要在训练过程中方便地计算得到。

在推荐场景下,可推荐的内容存在一定的时效性,随着热点的变化,用户的关注点也会发生相应的变化,导致CTR模型应用到线上后,预测性能会随着时间的流逝而下降,所以CTR模型都需要进行及时的更新。在不同的业务应用场景下,这个更新频率可以是分钟级,也可能是天级别的。然而,重新训练一个百亿规模的模型会消耗大量的时间和计算资源,为了以低廉的资源成本完成模型的及时更新,推荐场景下会采用在线训练的方式。所以计算框架需要支持多种在线训练算法。目前应用于在线训练的优化算法主要有Ftrl,Adagrad等。

高性能大规模并行机器学习框架

在我们的系统设计目标中有三个关键维度:1)千亿级模型参数;2)千亿级样本数据;3)高性能。

如何同时提高上面的三个维度的目标,我们需要仔细分析分布式计算过程。以现在常用的基于梯度下降的分布式优化算法为例。在使用样本数据I进行一轮训练的过程中,有以下几个基本步骤,1)数据分片,将所有数据拆分后分配到多台机器上;2) 并行计算g,各台机器上的计算节点按照指定算法计算梯度;3) 聚合g,将各台机器上计算的g收集起来;4) 更新w,使用上一步得到的g更新w;5) 广播w,将更新后的w传输给计算机器。

这样的学习逻辑通过将数据分布到多台机器上计算,有效地解决了样本数据量的问题。Hadoop和Spark都采用这样的逻辑进行机器学习,Spark由于RDD的方式充分利用内存来存储中间数据,大大提高了训练性能。但是在这样的训练逻辑下,存在两个问题:1) w被存储在一台机器上,限制了框架能够训练的模型的规模,只能是单机可存储的模型,以128G的内存的机型为例,10亿个参数的模型就达到他的存储极限了;2) w被广播给各个机器。由于是广播推送方式,当模型规模变大的时候,广播操作带来的带宽成本会急剧增加。以我们的测试来说,用Spark训练一个百万参数的模型时就发现性能难以忍受了。

以上分布式训练逻辑是梯度下降算法的逻辑,而现在机器学习尤其是深度学习中广泛使用的是随机梯度下降算法(SGD)。模型参数是以minibatch(128个样本,甚至更少)为单位来更新的。这导致参数更新频率急剧提升,带来的是巨大的网络带宽需求。所以必须要解决上面两个问题,才能够进行千亿级参数的模型训练。参数服务器架构由此产生。

参数服务器的基本结构和工作流程图

从2010年被提出,经过了几年的发展演进,现在普遍使用的是第三代参数服务器架构。相对于前面Algorithm 1的流程,参数服务器有两点主要的不同:1) 有一种新的角色Server,专门用于分布式地存储模型参数,并进行参数的更新计算。这使得能够训练的模型规模不再受限于单机的内存大小,同时将多个worker节点的参数请求分摊到多个server上,减少了单个server上因参数和梯度传输导致的网络瓶颈。2) 负责计算的Worker节获取参数的方式是pull方式。由于不是被动的等待广播参数,pull方式使得worker节点可以根据训练数据的需求来获取参数。尤其是在推荐场景下,样本都是非常稀疏的。举例来说,一个模型可能有100亿个特征输入,而具体到一个特定的样本,只会有几百个有效特征值。所以只需要获取与这几百个有效特征值有关的参数即可,而不需要传输整个模型。

简而言之,参数服务器架构下,多个server分摊参数存储和传输的压力,多个worker按需获取和更新参数降低了参数和梯度传输的网络需求。这使得千亿参数模型的高性能训练成为了可能。

通过上面的分析,我们得到了以下的结论。参数服务器能够在模型规模,样本数量和训练性能三方面满足我们的设计要求。

Hadoop/Spark/参数服务器对比

了解了通用的参数服务器架构以及其特点,我们回到无量计算框架,继续分析一个通用的参数服务器架构在实际中面临的问题以及我们的解法。

在模型和算法的分析中,我们知道,要实现两类稀疏和稠密两类参数的传输与更新。

1)超大规模稀疏的输入特征参数。这里稀疏有两层含义。

首先,模型可能的参数是千亿个,但是因为并不是所有特征都有可能出现在训练样本中,所以一般不会所有参数都有值,一般最终的模型可能只有1/10的参数是有值的。如果使用了稀疏化的技术,这个比例会更低。

其次,对于每个样本只会使用到非常少的特征。在一个千亿特征的模型中,单个样本通常只会命中到几百个特征。

从上面的分析中,可以看出,参数服务器架构在大规模稀疏特征的模型训练中尤为高效。因为worker训练一个minibatch的样本时,只需要获取与这些样本相关的参数即可,如果每个样本平均有500个特征,那么100个样本最多只需要获取5万个特征的相关参数即可。

2)稠密的交叉关系参数。与稀疏的输入特征参数不同,交叉关系参数规模相对较小,但是每个样本的训练会使用到全部的稠密参数。假设全连接层中最大的一层是1024*512,那么每次计算使用到的稠密参数就是在50万这个量级。

从这里我们可以看出,稀疏和稠密两种参数在训练过程中存在不同的性质。稀疏参数总体规模大,但是每次训练只使用到很小的一部分。稠密参数总体规模相对较小,但是每次训练都需要被全部使用到。由于两种类型参数的性质差异,被自然地切分成了基于Kv和基于矩阵的数据结构。

下面我们继续分析训练各个阶段的性能问题与我们的解法。

1)参数获取。在实际的超大规模模型的训练中,网络经常成为性能瓶颈。为了减少因为参数获取而导致的网络传输压力,我们引入了参数缓存机制,worker并不是每个minibatch都从server获取最新的参数。然而,在分布式训练中,缓存模型参数存在训练正确性的风险。由于在数据并行情况下,各个计算节点使用的训练数据是不同的,如果进行多次训练而不同步更新参数,则模型可能出现无法收敛的问题。在学术研究领域,这是一个训练的网络带宽与模型训练正确性保障的问题。已有不同的同步控制协议的研究。我们的实现借鉴了ssp协议[5]中有限版本差异的思想,通过控制缓存的使用次数,在保障训练正确性的前提下,减少因参数获取而导致网络传输。

2)梯度更新。计算完成后的梯度上传也会有大量的数据需要通过网络传输。按照模型的梯度计算逻辑,所有使用到的参数都会得到相应的梯度。但是,是否需要发送某个参数的更新,或者以什么样的方式发送给Server却是可以选择的,这个过程称为梯度压缩。梯度压缩的方法大致可以分两类:1)梯度量化。将梯度从double/float等原始类型量化成二值/三值等用几个bit就能表示的类型,以减少传输数据量。2)梯度稀疏化。选择重要的梯度立即上传,不是很重要的梯度更新,则累积起来,稍后再上传。如读者对这个研究领域感兴趣,可以阅读参考文献[6][7]。

传统的梯度压缩技术存在与模型大小相当的内存消耗,所以主要使用在单机可存储的稠密模型的训练中,在无量所应对的超大规模模型的训练中,我们对现有的梯度压缩技术进行了改进,使之适应了百亿稀疏参数规模模型的训练,可以减少99%以上的梯度传输而不影响训练效果。

3)梯度计算。在机器学习,尤其是深度学习过程中,模型的梯度计算过程会有大量的数值计算操作。除了使用多线程并行训练的方式充分利用多个cpu的计算能力,我们还使用SSE等CPU并行计算指令和Eigen线性计算库实现梯度计算过程,充分利用了CPU芯片的流水线和并行指令能力,比单纯的多线程并行的计算性能高10+倍。

在实际的生产环境中,数据被存放在hdfs集群上,而训练时拉取数据变得很耗时。我们通过将数据读取异步化,使得数据读取不影响训练的参数传输,梯度计算和更新过程。同时通过优化数据读取模块的内存管理和样本缓存机制,以极小的内存开销满足训练对样本随机性的需求。

3. 无量模型管理--全流程模型管理

在推荐类业务中,文章和视频资料快速更新,社会热点随时出现和消失,用户的兴趣也经常变化。为了取得优秀的推荐效果,很多具有时效性的特征信息被加入到预测模型中,导致CTR模型需要及时更新。无量系统提供了全流程的模型管理服务。

模型流转的基本流程

在管理超大规模的模型时,存在两个主要的挑战:

1)模型超大导致的模型上线性能的问题。对于千亿参数的模型,如果每个参数都以4字节的float格式存储,最终存储的模型将会接近TB级别。如果要实现分钟级别地将新模型更新到多地的在线预测服务上,仅从数据传输和文件解析的性能上看,每次都使用全量模型的方式就是不可行的。幸运的是,模型在训练过程中的变化是渐进的,而当模型上线时,是一个相对稳定的状态,在线训练更多的是对模型的微调。因此,对于超大规模的模型,一般采用全量+增量的方式进行管理。首先使用全量模型加载到线上服务,然后定期将模型的差异部分以增量的方式叠加到线上服务的模型中。

2)模型分片导致的管理问题。在全量+增量的模型上线模式下,线上服务的模型对应着多个模型文件。如果线上服务出现故障需要恢复或者因为请求量上升需要扩容时,就需要使用多个模型文件来恢复出模型。在某些情况下,业务发现当前模型效果差,需要替换模型或者进行版本回滚时,需要另外的一组模型文件。另外,不同于单机可存储的模型,在参数服务器框架下,模型被分片存储在不同的机器上。为了提高模型导出效率,多个server节点会并行导出多个模型分片文件。假设存在100个server,那么就会有100个模型分片文件。给模型管理工作带来了挑战。

为了避免模型开发和使用者陷入这些管理问题,同时也为了保障系统的稳定运行,无量模型管理服务将所有模型管理的相关工作承接下来。用户只需进行必要的配置,模型管理服务就会自动地发现新版本的模型,验证模型的完整性并将新模型传输和发布到指定的在线预测服务中。对用户完全屏蔽下层类似全量,增量,分片等细节。后期,用户还可以自定义模型验证的方法,对即将上线的模型进行模拟请求等校验,避免有效果差的模型被上线,给业务造成损失。

4. 无量模型服务

使用千亿参数的大模型进行线上预测,面临有许多的问题,下面我们就一些主要问题进行分析并介绍我们的方案:

1)模型加载的内存问题。当被加载到内存中时,需要构建相关的数据结构,所消耗的内存大小会比模型文件大很多。以最简单的LR模型为例,每个特征只会有一个float类型的模型参数,一个10亿有值特征的模型的文件大小大概是12GB(每个特征8字节key+4字节值value)。使用stl标准库中unordered_map加载这个模型需要超过25GB的内存。也就是说会有超过模型大小1倍的内存开销,这使得单机能够存储的模型大小受到极大的制约。我们自己实现了一个hash_map:tl_hash_map,专门针对模型稀疏参数特点进行了内存优化。内存消耗只比模型数据大20%左右。这意味着tl_hash_map有效地提高了能够被单机存储的模型的大小极限。以128GB内存的机器为例,使用tl_hash_map,最大能支持的lr模型文件大小是100GB左右,而标准unordered_map最大能支持50GB左右。

2)模型服务的性能问题。为了达到良好的用户体验,预测服务的响应时间需要在10ms这个量级。以手机用户的推荐场景为例,从用户在手机上刷新页面到看到推荐结果,时间不能超过1s,扣除掉网络通讯的开销,IDC内在线服务的响应时间需要控制在200ms以内,而整个推荐的流程至少有召回,排序和展示控制三个阶段。在排序阶段,需要对200个以上的文章进行特征拼接和点击率预估,所以模型对这200个文章进行点击率预估的总时间要在30ms以内。

从排序服务发出请求开始,到请求完成,至少存在两个性能瓶颈点:

1)请求包的网络传输与编解码。为了预测文章的可能点击率,需要为每个文章准备所有的样本特征。假定每个样本有500个特征,那么200个文章的请求就有10万个特征。整个请求包的数据会有1MB左右。网络传输和编解码的性能对整个rpc框架都带来了极大的挑战。我们定义了一套针对模型预测场景的特征编解码格式,避开了现有rpc框架在编解码格式上的性能缺点,并且最大化地减少了需要传输的数据大小。

2)模型参数查询和计算性能。为完成模型的预测功能,首先需要从模型中找到需要的参数,然后完成预测值的计算。面对超大规模的模型,首先要解决的就是模型存储方式的问题。如果模型能够单机存储,那么模型参数的查询则可以在本机完成。如果模型超过单机存储的极限,则需要使用分布式存储的方式提供查询服务。考虑上面的例子,一个请求需要10万个特征的参数,这些特征被存储在多台机器上。即使忽略预测计算时间,要保证这个请求在30ms之内返回,那么所有存储参数的节点都必须在30ms之内返回结果。这就会出现木桶现象,任何一个存储节点出现了超过30ms的响应延时,总体请求时间都一定会超过30ms。这样的存储系统对请求排队是接近0容忍的。但推荐场景又是一个高并发的场景,预测服务需要支持每秒上万的用户请求。无量系统开发了一套分布式模型预测服务,专门针对分布式预测场景下高并发的模型参数请求的性能问题进行优化,实现对TB级模型的高并发预测服务支持。

5. 总结

随着互联网服务的发展,越来越精细和定制化的服务需要更好的模型支持,而超大规模预测模型已经成为主流的解决方案。通过深度的研究与优化,无量系统开发了能够支持千亿级参数模型训练的高性能计算框架,并通过模型管理,模型预测服务,实现了超大规模模型的训练,管理以及上线的全流程支持。无量系统已经支持了LR/FM/FFM/DNN等多种常用模型,并在移动手机浏览器业务中实际使用和验证,帮助业务取得了巨大的业务指标提升。无量系统将逐步扩展功能,比如正在探索的基于GPU的深度学习技术,以覆盖更多的现有业务场景以及最新的AI类应用场景,为业务的进一步提升提供强大的系统支持。

参考文献

[1] Hidden Technical Debt in Machine Learning Systems, Google. In NIPS'15 
[2] Wide & Deep Learning for Recommender Systems, Google 2016
[3]常见计算广告点击率预估算法总结 
[4]深度学习在CTR预估中的应用
[5] Solving the stragglerproblem with bounded staleness. In HotOS (2013).
[6] TernGrad: Ternary Gradients to Reduce Communication in Distributed Deep Learning
[7] Deep Gradient Compression: Reducing the Communication Bandwidth for Distributed Training

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

扣心自问,这样的文章对得起 “揭秘” 两个字吗 ? by 梁 爽

先啰嗦一堆背景,我们要解决的问题很复杂,我们解决了这个复杂的问题,我们牛逼吧?这样装逼真的好吗?

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

1 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT