0

平稳突破单机房容量瓶颈,B站离线多机房架构实践 - 架构 - dbaplus社群:围绕Data、Bl...

 1 year ago
source link: https://dbaplus.cn/news-141-5031-1.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

平稳突破单机房容量瓶颈,B站离线多机房架构实践 - 架构 - dbaplus社群:围绕Data、Blockchain、AiOps的企业级专业社群。技术大咖、原创干货,每天精品原创文章推送,每周线上技术分享,每月线下技术沙龙。作者介绍

陈昱康,哔哩哔哩技术专家,B站离线平台负责人, 对分布式计算和存储、调度、查询引擎、在线离线混部、高并发等方面有丰富研发和实践经验。

陈诚,哔哩哔哩资深开发工程师,2020年加入B站基础架构部,现服务于大数据实时团队, 先后负责离线HDFS存储,实时降本增效(混部、Flink弹性伸缩)、Flink on K8s、数据传输等工作。

一、背景

随着B站业务的高速发展,业务数据的生产速度变得越来越快,离线集群规模快速膨胀,既有机房内的机位急剧消耗,在可预见的不久的将来会达到机房容量上限,阻塞业务的发展。因此,单机房容量瓶颈成为了我们亟待解决的问题。

目前,针对机房容量问题的解决方案业界主要有以下两种:

1)集群整体搬迁至更高容量的机房(scale up)。该方案是一种纵向扩容方案,即将现有集群搬迁至容量更大的机房,从而提供集群扩展的空间。现实中,集群迁移一般不能影响业务的发展,即保证不停机,因此,迁移过程中需要两个规模相近的集群做全量迁移,或者需要一个具有一定规模的过渡集群,分批次迁移;对于大规模(tens of thousands)集群来说,迁移的经济成本巨大;另外,迁移后的新机房会有再次达到容量上限的风险。

2)多机房方案(scale out),即一个机房容量有限,扩展为多个机房,同时对既有架构进行一定的改造,保证用户视角仍像是一个机房。此举可依据业务需要,采用灵活的方式增量扩容,从而一定程度上避免容量冗余问题。然而,该方案会存在跨机房数据交互,而机房间网络带宽一般也存在瓶颈;同时,网络的抖动或断网可能造成跨机房业务出现异常。因此,该方案需要考虑/解决网络带宽不足及网络抖动/断网问题带来的影响,技术成本较集群整体搬迁方案要高。

就我们目前自建机房的情况来看,中短期暂无清退既有机房(全部搬迁至新机房)的计划,从长期来看也会存在多个机房;另外,比起方案2的技术成本,我们更难接受方案1的经济成本和容量风险。因此,方案2是我们解决机房容量问题首选方案。

二、多机房方案



1、面临的问题

1)带宽瓶颈

离线场景主要是批处理场景,是对海量历史数据进行离线分析/处理的场景,该场景对延迟不敏感,但由于其处理数据量巨大对网络带宽等资源消耗较大;另外,生产场景中作业数量一般较多且执行时间不受控,若两个机房的主机只是简单叠加在一起作为一个集群来用,可能会存在大量的跨机房互访,产生大量的随机流量打满有限的跨机房带宽, 此时除离线自身受影响外, 还可能对其它跨机房业务造成影响 。因此,如何防止跨机房随机流量打满跨机房带宽,是多机房方案要解决的一个重要问题。

2)网络抖动&连通性

跨城网络会受供应商服务质量影响(或施工影响)造成抖动(或断网), 与机房内CLOS架构的网络质量相比会低很多。若两个机房的主机当做为一个集群来用,如图1 HDFS示例,当网络抖动时,不但会导致跨机房读写延迟增加,还会影响DN的IBR等过程,造成服务性能和稳定性下降;当网络出现严重问题造成断网时,会导致异地机房数据不可用,还会导致异地机房DN失联,造成大量Block低于预期副本数,触发NN大量补副本等问题。因此,如何降低网络抖动及网络连通性问题带来的影响是多机房方案要解决的另外一个不可忽视的问题。

图片

图1 HDFS 架构

2、设计选型

如上所述,多机房的主要矛盾是跨机房网络带宽不足、稳定性差与离线海量数据处理任务高效产出之间的矛盾,解决该主要矛盾面临的核心问题是如何减少跨机房带宽的消耗,以及如何降低网络稳定性问题带来的影响。

经调研,单元化架构是为解决多地多中心问题演进而来的部署架构,其中,单元是指一个能完成所有业务操作的自包含集合,在这个集合中包含了业务所需的所有服务,以及分配给这个单元的数据[1-2] 。按照单元化的思路,在多机房场景中,每个机房可以作为一个单元,每个单元内提供作业执行所需要的全部服务以及数据,保证作业在单元内完成,从而解决上述多机房面临的核心问题;单元化拆分后任何一个单元的故障只会影响局部,不会造成整体瘫痪;在选定采用单元化思想来设计了多机房方案之后, 多机房方案的核心问题就限定在了如何决定作业与数据放置,以及如何让作业访问距离近的数据,来降低跨机房带宽的消耗及网络稳定性问题带来的影响。

带着上面的核心问题,我们调研了业界大厂的多机房解决方案[3-7]。这些方案在计算层面为防止Shuffle等中间结果数据造成跨机房流量,每个机房均独立部署了计算集群,在该层面均符合单元化思想;但在存储存面存在分歧,如图2所示,依据数据和异地机房的数据副本是否属于同一组NameSpace (NS),大体可以分为多机房单集群方案和多机房多集群方案。

图片

图2 多机房方案分类

[3-5] 采用了多机房单集群方案,该方案中采用Block级的数据副本,数据和数据副本同属于一组NS,无数据一致性问题,但因NS只能在其中一个机房,无法有效应对网络连通性问题,且Namenode异地副本管理(BlockPlacementPolicy)和相关工具(Mover, Balancer等)改造成本较大,另外该方案可扩展性也受单集群规模制约。[6-7] 采用了多机房多集群方案,整体符合单元化思想。其中 [6] 应用于云梯迁机房场景,它首先在同机房中通过Fast Copy将文件元数据分离到两个NS,然后再通过同NS内DN到DN的跨机房Copy将数据复制到远程机房,该方案在一定程度上可以有效应对跨机房网络风险,但因存在两次copy时效性上难以保障,另外也存在异地的数据节点,因此本质上也存在多机房单集群方案改造成本和扩展性问题;[7] 阿里Yugong(Yugong: Geo-Distributed Data and Job Placement at Scale)基于MetaStore针对分区表场景,通过调整作业放置和数据放置来降低跨机房带宽的消耗;如图3所示,计算A、B存在跨机房访问行为,通过调整(互换)计算A、B的放置位置可以有效减少跨机房访问流量;计算C、D同时跨机房消费同一份数据3, 若通过数据复制的方式将数据3复制到机房2, 让C、D依赖数据3在机房2中的副本,则可以减少一次跨机房消费数据流量。但对于我们采用开源大数据架构的场景来说,需要改造(分属于多个子部门的)多种计算框架来适配其基于MetaStore的数据副本管理和数据路由,改造实施成本较大;另外,其基于MetaStore的设计只能解决表(SQL)场景的多机房问题,也不能覆盖我们对非表场景提供多机房支持的需求;不过,该方案中通过“作业放置-数据复制”来解决带宽瓶颈问题的思路非常值得我们借鉴。

图片

图3 任务跨机房随机分布

综上,我们参考Yugong“作业放置-数据复制”的思路,采用有限的单元化思想设计多机房方案;如图4所示,每个机房部署一套独立的完整的集群(YARN&HDFS),为作业在一个机房内执行提供最基本的服务保障,从而在跨机房网络出现异常时,降低影响范围;同时,通过合理的作业放置和有计划的数据复制,消除跨机房随机访问流量及跨机房数据重复消费等问题,来达到降低带宽消耗的目的;另外,我们结合内部的基础设施情况,以及满足表和非表两种场景的需求,我们选择了基于扩展HDFS Router(RBF)多挂载点来实现数据副本管理和数据路由功能,并通过Client IP感知自动将数据请求路由至较近的机房;还有为解决数据复制带来的一致性问题引入了Version服务等,图中涉及组件将在实现部分进行介绍。

图片

图4 多机房架构

3、总体流程

图5展示了以Hive作业为例的在上述设计思路下的总体流程,图中绿色模块为我们新增或改造组件。首先,通过周期性的分析作业间依赖关系及依赖的数据大小,确定作业放置位置信息并进行持久化(DataManager用于管理作业放置信息等),B站的作业调度平台(Archer和Airflow)提交作业时,先获取作业的放置机房信息,并检查预期放置机房的数据副本是否Ready,若Ready则提交作业,否则,阻塞提交,等待数据复制服务完成复制数据;其次,作业调度提交后,拉起Hive/Spark Driver生成可执行计划,向预期DC的Yarn集群提交Job,等待拉起Job,同时我们在Yarn层面也做了改造,基于Yarn Federation架构,实现了基于app tag和队列的机房调度策略,这个在下文也会介绍; 最后,被拉起的作业请求HDFS数据,HDFS Router依据Client IP所属的DC信息,自动将请求路由到距离Client较近的数据复本所在机房的NS, 并将结果返回Client。

图片

图5 多机房作业调度执行流程

多机房核心流程包括作业放置、数据复制、数据路由、版本控制、数据限流、跨机房流量分析等几个阶段,上述Job提交流程并未完全涵盖,下文实现部分我们将对所有阶段进行详细说明。

三、多机房方案实现

下面章节会对多机房核心环节进行介绍, 包括作业放置、数据复制、数据路由,以及为保障数据副本一致性引入的数据版本服务和带宽控制的限流服务,并引入事后的跨机房流量分析工具,用以发现预期外的跨机房行为指导调整。

1、作业放置

1)依赖分析

大数据离线场景,作业数量多,作业之间依赖复杂。比如,大数据离线报表处理业务,从数据采集,清洗,到各个层级的报表的汇总运算,到最后数据导出到外部业务系统,一个完整的业务流程,可能涉及到成百上千个相互交叉依赖关联的作业。就作业放置来说,对复杂作业依赖的管理和分析工作至关重要, 而如我们自研的调度平台Archer等DAG工作流类调度系统,自身具有较强的作业依赖管理能力,因此,我们仅需要聚焦作业依赖分析以确定要迁移的业务。

我们依据作业间依赖关系及需要处理的数据大小,基于社区发现(Community Detection)探索了一种考虑跨机房带宽代价的作业关系链划分模型。该模型首先依据调度系统管理的作业间的依赖关系构建DAG图, 然后从DAG图中圈出相对高内聚(相对比较闭环)的业务子单元,最后结合相互依赖的子单元间的数据量选择出的可以迁移的子单元;如图6所示的简单DAG, 我们假定图中正方形代表计算,圆形代表数据,圆的大小代表数据大小,则我们以虚线作为划分边界将DAG分成两个子单元,分别调度到两个机房,则可满足数据传输代价小的目标。当然,整个过程除了考虑跨机房数据访问代价外,还需要考虑机房计算和存储资源是否可以满足需求。

图片

图6 依赖关系划分

一般而言,实际生产中的ETL等周期性调度作业相对比较稳定, 不会频繁发生变化,甚至部分作业不会出现变化,因此,确定Job放置在那个机房的的依赖分析过程可以以天或周为单位周期性的离线计算产生;另外,从管理的角度来看,公司一般会有多个相对比较独立的业务部门,每个业务部门又会垂直地划分出多个业务子单元,业务内的作业间联系紧密程度远大于业务之间;同时,业务(单元)也是资源管理单元,以及多机房落地实施过程中的沟通单元;因此,在实践中往往是以业务单元为边界进行依赖划分。

2)作业放置

我们的生产环境中存在多个作业调度平台,如Archer、Airflow等平台,将Job放置在那个机房的信息维护在任一平台都不能涵盖所有作业, 因此我们引入DataManager服务(在整个体系中的位置见图4)作为接入层,用来管理作业放置的IDC信息和需要进行数据复制的路径信息,Archer/Airflow等调度平台通过对接该服务来接入多机房体系;下面以自研DAG调度平台Archer为例描述工作流程如下:

  • 前置工作:Archer 通过DataManager接口设置作业的放置位置信息,以及依赖数据的路径pattern、范围、生命周期等信息。



  • Archer访问DataManager确定作业放置的IDC信息,并为作业选择符合IDC作业配置信息。



  • Archer询问Job在该IDC的数据是否Ready,若Ready,则设置app tag为该IDC并通过Yarn RMProxy向计算集群提供作业;否则,挂起并等待数据Ready后尝试重新提交;其中数据是否Ready,是通过DataManager转发请求至数据复制服务得到。

另外由于我们的业务部门和Yarn上的一级队列做了一一映射,所以一旦某个业务部门的数据整体迁移到新机房后,我们会在RMProxy中设置该部门对应的queue mapping策略到新机房,这样无论是从调度平台还是其他用户客户端提交的Job即使没有接入DataManager也能正确路由到新机房的计算集群,同时回收老机房的计算和存储资源。

图片

图7 Yarn调度策略

2、数据复制

1)复制服务

作业放置会将有联系紧密的Job放在一个机房,以减少跨机房访问,进而减少跨机房网络带宽消耗;对于无法消除的跨机房依赖,特别是异地机房使用频次大于1的数据,需要异地机房也存在数据副本,以降低网络带宽消耗;因此,我们提供了数据复制服务来进行副本复制。

数据复制服务基于社区提供的DistCp工具实现, 并在正确性、原子性、幂等性、传输效率等方面作了增强, 同时支持流控、多租户传输优先级(高优作业能得到更多跨机房流量和计算资源配额),副本生命周期管理等功能。

2)复制流程

数据复制主要针对有规律的周期性调度作业进行,这类作业一般比较固定,通过对作业历史运行记录进行分析即可推测出作业的输入输出情况,包括数据路径和使用的数据范围(防止长时间跨度回刷任务大量复制)等信息。因此,当确定好待迁移的作业后,可以提炼出数据路径规则(rules),并持久化到DataManager的规则库中(规则库会随作业放置的变化而进行周期性更新)。

然后,针对不同的场景使用规则库进行路径抽取,下面以Hive表场景为例描述数据复制流程,如图8所示, 首先收集Hive MetaStore的挂载表/分区相关的Event信息至Kafka服务,然后通过实时Flink任务清洗出符合上述规则库中规则的路径,当检测到热点表的新分区生成后,交由数据复制服务(DRS)进行传输,生成异地机房副本,DRS本质上是一个DistCp作业的管理服务,在传输完成后由数据复制服务持久化副本信息(包括路径、版本、TTL等),以对副本数据进行全生命周期管理(删除过期的跨机房副本,释放存储空间),目前B站线上有100+张Hive热点表路径设置了跨机房副本策略。

图片

图8 数据复制流程

上述复制流程采用自动发现主动复制的策略,可以快速捕获并准备数据副本,经过统计在我们的生产中数据副本延迟的PT90可以控制在1min以内, PT99 在5min以内,可以有效满足离线场景的业务需要;然而,上述自动发现主动复制的策略,可以有效解决增量数据副本的问题,但对于待迁移作业来说,可能还依赖较长一段时间的存量数据,针对该问题,我们除了采用提前启动复制流程的方式准备存量数据外,还针对需要快速迁移的场景引入了基于Snapshot的数据迁移策略进行初始复制,因Snapshot为社区成熟技术不再缀述。

3、数据路由

上小节介绍的数据拷贝后双机房均会存在某路径的数据副本,当作业放置到IDC后如何定位到正确的数据是数据路由服务要解决的关键问题;

我们在《HDFS在B站的探索和实践》中提到的基于HDFS Router的多挂载点实现的MergeFs的基础上,实现了镜像挂载点来实现数据路由功能。为方便描述,我们约定原始数据为主数据, 传输到异地机房的数据为副本数据(也称为镜像数据,该数据只允许读取和删除),并且约定镜像挂载点中第一挂载点为主数据,之后的挂载点为副本数据(理论上可以扩展多个机房),为了在路由层面做到对用户透明,我们在镜像挂载点的处理逻辑中,增加了请求来源的IP位置感知功能,该功能能过获取请求来源IP的位置信息,判断请求来源的DC并将请求路由到相应的DC的HDFS。如图9示例所示,若数据请求来自DC1, 则Router将数据请求重定向到DC1的HDFS集群,来自DC2则定向到DC2的HDFS集群(图中同种颜色线条标识请求路径)。

图片

图9 基于Router的数据路由

为了降低跨机房带宽的消耗,原则上,我们规定所有对数据的读取操作,都只允许在本地机房(即Client所在机房), 否则先拷贝到本地机房。但特殊情况下,如图10所示,若Data Replication Service发生异常短时间无法修复或ns长时间异常时,则我们允许降级为跨机房限流读(副本未ready情况, 超过一定的时间未在目标机房读取到数据,则降级),限流部分在后面章节进行详细介绍。

图片

图10 数据路由容错

另外 ,由于历史原因,在我们的生产中存在一种特殊的临时库,用于管理用户SQL作业中的创建的短生命周期的临时表(Temporary table,七天自动清理),该类临时表表名不固定(例如一些ETL作业会在临时表名上加上日期后缀),也就造成了表类路径不固定;针对该类路径不固定的情况,无法使用上述镜像挂载点进行管理,因此, 我们引入一种名叫IDC_FOLLOW的多挂载点,用于挂载多个机房中的临时库路径;当读写临时表时,会依据Client所在的DC选择DC内HDFS NS挂载路径来存取数据,从而解决临时表跨机房流量的问题。

4、版本服务

分布式场景下,通过数据复制方式产生副本,不可避免会导致一致性问题,因此,多机房存在数据副本时,除了涉及上述路由选择问题外,还必须考虑数据版本一致性问题,我们通过引入版本服务(Version)解决该问题;为了简化版本服务设计, 针对大数据离线场景写少读多的特性,我们依据CAP理论对镜像挂载点的实现做了一定的取舍,规定了对主数据可以进行所有操作,副本数据只允许读/删操作;在这个前提下,我们引入了基于HDFS Editlog的版本服务,如图11所示,该服务以观察者的身份监控向HDFS JournalNodes(JN)订阅路径的变更行为,并以操作ID(transaction id)来标识数据版本;若订阅的路径中数据发生了变化,则会通过editlog传导到JN,再由JN通知Version进行版本更新;因所有对数据的变更操作都会记录editlog,因此,不论SQL场景和非SQL场景,只要数据存在变化均可被版本服务捕捉到,从而可以有效保证数据的一致性。

图片

图11 数据版本工作流程

上文2.3节总体流程所描述的提交作业时,当获取到作业预期的放置机房后,检查依赖数据是否Ready的工作也包括版本检查工作;当作业需要副本数据时,会通过数据传输服务检查已传输的数据副本的版本与版本服务中订阅的最新版本是否一致,若一致允许作业提交使用数据副本;否则,作业临时阻塞,待传输服务更新副本数据后,则允许提交作业;若超过一定的时间未在目标机房读取到数据,则降级为读取主数据。

5、限流服务

我们的场景下跨机房带宽有限(约4Tbps),并且和在线服务、实时服务等对延迟更敏感的服务共用带宽,为防止离线跨机房流量(特别是计划外的跨机流量)打满带宽影响在线业务, 我们引入了基于令牌桶的限流服务。

图片

图12 令牌桶

令牌桶限流的核心思想为当进行某操作需要令牌时,需要从令牌桶中取出相应的令牌数,如果获取到令牌则继续操作,否则阻塞,用完之后不用放回。基于该思想我们设计了全局中心限流服务,我们在HDFS DistributedFileSystem类基础上,实现了具有读写限流功能的ThrottledDistributedFileSystem,当用户使用该类去读写HDFS的文件时,ThrottledDistributedFileSystem会根据RBF返回的LocatedBlock中的client IDC信息和Block IDC信息,判断此次读写流量是否会跨机房,如果是会先尝试向ThrottleService发送申请跨机房带宽请求(Token),申请到Token后,再进行后续的HDFS读写,如果申请的流量用完后,再向ThrottleService申请新的带宽Token;除利用令牌桶固有的特性外,我们在令牌桶的基础上实现了队列优先级和加权公平特性,限流服务的队列优先级和调度系统中的作业优先级也做一一映射,来保障多租户情况下重要服务可以优先获取到Token;在稳定性方面,为了降低限流服务的压力,我们设置每个Token代表相对较大的流量单元,来降低Token的获取次数过多带来的性能影响;为防止限流服务宕机导致作业阻塞,我们增加了降级为本地固定带宽的策略,同时随着计算引擎持续接入限流服务,服务本身的稳定性和请求水位成为瓶颈(单机100K+ qps),我们通过水平扩展服务的方式增强了限流服务的性能。

6、跨机房流量分析

随着多机房项目的逐渐推进,跨机房流量也日渐增长,高峰时刻偶尔会打满专线带宽。为了对跨机房带宽流量进行有效管控,我们需要了解哪些作业贡献了最多的跨机房流量,从而进行针对性治理。从离线作业的角度看,网络流量来源主要有三块:

  • 从上游读取数据

  • 作业执行过程中不同Executor/Task之间shuffle数据

  • 写数据到下游表

在B站多机房的场景中,因为采用单元化架构每机房均存在独立Yarn的集群,作业不会跨机房运行也就不存在跨机房Shuffle数据的情况,因此只需考虑读写HDFS文件过程中产生的跨机房流量即可,而读写HDFS文件产生的跨机房流量又可以分为计划内流量和非计划内流量两大类:

计划内流量:3.2 小节所述数据复制服务进行数据副本复制产生的流量,我们称为计划内流量, 该部分数据大概率会被多次使用。

非计划内流量:即非数据复制服务产生的数据流量,单次(或多次)使用,主要来源有以下几种可能。

  • 计划内的调度任务发生长时间跨度的历史回刷,依赖的数据副本已过期销毁



  • (漏迁/错迁/新增等)放置位置不合理的周期性调度任务,可以通过优化作业放置消除



  • Adhoc查询,突发流量, 单次(或多次)使用,临时生产需求,无法预知需要的数据,无法预先进行处理

1)流量分析工具

在实际生产过程中,非计划内流量不可避免,为了对跨机房流量进行有效管控,我们引入了跨机房流量分析工具,我们在引擎端和DN端做了以下改造:

  • 引擎端: 在初始化HDFS Client时将作业JobId注入到DFSClient的ClientName中



  • DataNode: 在DataXceiver中埋点,从ClientName中解析出JobId,并按JobId和client ip网段合并读写流量,每30s输出统计结果到流量日志中

我们将每台DN上的跨机房流量日志进行实时收集,通过Flink汇总到ClickHouse上,然后聚合分析得出每个时间段的跨机房流量作业Top10,方便对跨机房流量进行治理(包括重新放置、紧急查杀、作业优化等)。

图片

图13 流量日志收集链路

以下是我们跨机房流量分析的监控面板:

图片

图14 跨机房流量分析

2)Adhoc流量治理&优化

对于Adhoc类型的非计划内流量,因为其随机性,本文所述多机房体系中“数据复制-作业放置-数据路由”方式不适用;因此,我们采用一些其它的优化手段, 比如通过运行时SQL Scan扫描出依赖的数据大小、位置信息,以节省多机房带宽为最主要目标,结合集群的实际负载情况,决定SQL调度哪个机房,比如:

  • 访问单张表:作业调度至数据所在机房。



  • 访问多张表

多表在同机房, 作业调度至数据所在机房。

多表在不同机房, 作业调度至数据量较大的表所在机房;较小表限流读,或者阻塞通知拷贝服务拷贝。

另外, 对于Presto这种有多源查询能力的引擎,我们利用其Connector多源查询功能力将每个机房视为一个Connector,在多表访问场景中将子查询下推发送到远端机房进行处理,以减少垮机房流量带宽,详情见《Persto在B站的实践》5.2节多机房架构。

四、小结&展望

本文描述了B站离线多机房方案,该方案已平稳上线运行半年以上,迁移数据量近300PB,作业数占集群所有作业数的1/3。从实践的结果来看该方案在很大程度上解决了跨机房网络带宽不足、稳定性差与离线任务高效产出之间的矛盾。鉴于当前部分大数据关键组件的单元化进程,在抗网络连通性风险方面的能力还有较大的提升空间,后续我们将不断地推进单元化进程,进一步降低网络问题的影响范围,同时赋予部分高优化作业“双活”的能力。

另外随着新机房的持续扩容(老机房无法扩容,新增节点都会部署在新机房),我们也需要持续迁移更多作业到新机房,为了提高迁移的推进速度,需要尽量减少对上下游业务方的依赖(如:请求业务方协助对子业务进行划分和梳理),因此,我们需要实现更智能更自动化的待迁移数据和作业的自动划分流程,进一步强化使用社区发现(Community Detection)算法将DAG能自动划分成多个内聚性较高的子集/社区,按照社区粒度进行迁移的工作。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK