〇、序章

  互联网用户的谈论宣布和谈论浏览这类UGC场景作为普通用户的中心交互体会已成为UGC产品的标配,从BBS/贴吧跟帖到微博的“围观改变中国” 概莫能外。
  从公司第一款产品“搞笑囧图”开端,谈论服务就现已相伴而生。目前全公司已有X产品线接入谈论服务,仅国内日增谈论量X亿+,谈论列表接口晚顶峰QPS可达Xk+。
  为了给各业务场景供给更灵活的数据模型以及更高效更为安稳的服务才能,谈论组从2018年四月上旬开端对谈论服务从底层数据模型到全体服务架构进行了彻底的改造重构。从项目启动至今刚好满一年,现在回头看有收益也有惋惜,在此简述一二供我们参阅。其间服务重构直接收益如下:

  1. 数据模型笼统: 将谈论(Comment)和回复(Reply)笼统成一致的谈论模型;
  2. 存储结构优化: 从之前Redis—MySQL(SpringDB)到现在的LocalCache—Redis—ABase+MySQL 三级存储;
  3. 服务功用优化:
    • 谈论列表接口(GetComments)晚顶峰pct99从100ms 降至60ms;
    • 谈论宣布接口(PostComment)晚顶峰pct99从250ms 降至25ms;
    • 全服务占用CPU 从7600核降至4800核;
  4. 公共轮子产出:

一、一年前的窘境与挑战

  因历史业务需求老谈论服务数据模型在底层数据库表结构划分红了谈论(Comment)和回复(Reply)两级,而数据结构反向作用于产品形状,导致限制公司各APP的谈论都是两级结构。仅有的区别就在于二级谈论的展示形状是以“今天头条”为代表的谈论详情页样式,或是“抖音”的楼中楼样式。谈论体系本身的数据结构不应成为束缚产品形状的瓶颈,因此供给更笼统的数据模型已支撑未来更为丰富的业务场景是这次重构的首要方针。
  老谈论服务数据存储中选用MySQL作为落盘存储,除谈论Meta字段外还包含Text和Extra字段,这两个字段容量占据总容量的90%,造成了很多的无效IO开销和DB存储压力。
  老谈论服务中对MySQL根据GroupId进行分库键拆分红101个分库,并经过SpringDB进行CommentId到GroupId的映射,当SpringDB(后期已不再开发迭代)出现问题时会出现严峻的读扩大问题;
  老谈论服务数据层与业务层逻辑耦合严峻,不利于确保针对各产品线业务逻辑迭代,以及后续的功用调优和运维确保;此外谈论服务存储组件拜访权限不收敛,导致多次线上问题以及运维成本的增加;
  老谈论服务的谈论计数依靠于CounterService,全体写入链路中间环节过多(comment_post_service → MySQL → canal(binlog) → Kafka(upstream) → Flink → Kafka(downstream) → CounterService)。导致的问题有数据更新存在≈10s延时;链路过长导致安稳性下降以及监控缺失;数据校验修正困难;数据读取不收敛等一系列问题;

二、新服务架构

2.1 数据模型规划

谈论服务数据模型演化阅历了三个阶段:

  1. 第一个阶段是单拉链方法,将文章ID作为拉链链表的表头以此拉取该文章下一切谈论内容;
  2. 第二个阶段是多叉树方法,对某条谈论进行谈论动作在数据结构上就是向下分叉出一个叶子节点。以老服务为例,数据选用分表存储的方法分红group_comment和reply_comment表,别离对应谈论(Comment)和回复(Reply)两级结构;
  3. 第三个阶段是森林方法,当多叉树各子节点需求打标/染色时,根节点就会从GroupId泛化成为GroupId+Tags。
    • 在新数据模型中,将谈论和回复两级结构笼统成独立的单行Comment结构,经过Level、GroupId、ParentId维护之前的树形结构。其间文章(CommentId=GroupId, ParentId=0)为Level0,老谈论(ParentId=GroupId)为Level1,老回复(ParentId=Level1_CommentID)为Level2。新表字段及其与老表字段映射逻辑可见附录;
    • 一起为了将来各业务线的存储阻隔,以及数据阻隔、数据单向阻隔、数据互通做准备,在相同GroupId/ParentId前提下,经过AppId和ServiceId(未来将经过AppId中提取ProductId后进一步笼一致个SourceId)进行数据阻隔。终究,谈论数据模型的根节点将由GroupId+SourceId组成;

2.2 全体架构规划

  在老服务中服务被拆分红comment_post_service(写)和comment_service(读)两部分,两个子服务以及谈论推荐都能直接拜访包含MySQL在内的各存储组件,此外包含推荐、审阅在内均可拜访在线MySQL库。为此新谈论服务将存储组件全部收敛至全新的Data服务内部,全体架构拆分红Post(&PostSubsequent)、Pack和Data倒三角结构:

  • Post(&PostSubsequent): Post服务承当包含宣布、更新、点赞在内一切写入相关的业务逻辑;
  • Pack: Pack 服务承当一切浏览相关的业务逻辑,包含谈论列表中推荐Sort服务的调用以及相应打包操作;
  • Data: 其间Data服务剥离简直一切业务逻辑作为一个朴实的CURD层作业,一起也借此机会收敛了一切根底存储组件的读写权限;其间MySQL仅保存Meta字段作为拉链索引,ABase保存全量数据供在线场景运用。

  如前文所述,老服务中心存储MySQL中所含Text、Extra字段占据较大的存储空间,在可预见的未来将会成为严峻的功用瓶颈。在重构服务中落盘存储分红异构的MySQL+ABase两部分,其间MySQL作为中心拉链场景仅保存Meta字段,而ABase中保存Meta+Text+Extra三部分数据。由于对ABase存储中选用Protobuf打包,再加上ABase内部的snappy压缩策略,预计可节约30%+的带宽及存储开销。
  此外谈论重构中供给comment_go_types公共库,用于收敛三个服务的公共逻辑、公有数据结构以及业务字段的一致办理;

2.3 Post服务简述

  在谈论写入场景中,Post服务拆分红Post和PostSubsequent两个子服务。拆分后PostComment接口晚顶峰延时从250ms降至25ms,既一个数量级的优化进步。
  Post服务承当包含参数校验、文本查看在内的前置业务逻辑以及调用Data服务写入数据。在此前的业务需求迭代中老Post服务对外暴露了12个接口,新服务中在坚持接口不变的前提下收敛逻辑为Post、Update、Action三大逻辑。因谈论宣布、谈论更新以及谈论点赞这类写入逻辑流程相似,在Post服务中选用WorkFlow串联各业务流程。

func PostComment(ctx context.Context, request *post.PostCommentRequest) (*post.PostCommentResponse, error) {
	workFlow := postCommentWorkflow{
		request:  request,
		response: rsp,
	}
	rsp, err := workFlow.init().check(ctx).process(ctx).persist(ctx).subsequent(ctx).finish()
	return rsp, err.ToError()
}

  PostSubsequent服务担任在谈论成功落库后其他的业务逻辑的处理。由于数据落库后本次调用中心流程就现已完毕,所以Post服务以OneWay的方法调用PostSubsequent后对上游回来成功。在PostSubsequent服务内部经过EventBus组件(EventBus组件于facility库内供给)进行各子使命(Task)的分发办理;在EventBus中经过EventType注册履行不同的Task,而Task本身分为Subscribe(同步串行使命)SubscribeAsync(异步使命)SubscribeParallel(异步并行使命)三类;
  此外针对谈论Emoji白名单检测需求,新Post服务经过Trie树结构完结谈论文本的Emoji字符检测。此需求延时从此前调用Antidirt服务的5ms降至1ms以内;

2.4 Pack服务简述

  Pack服务是承当谈论一切浏览相关的业务逻辑的打包服务,基本业务逻辑能够拆分红Load和Pack两部分。Load部分担任从下流依靠中获取本次服务调用中所需的原始数据,Pack部分将Load获取的原始数据进行业务逻辑相关的打包操作。
  Load部分经过ParallelLoader组件(ParallelLoader组件于facility库内供给)进行依靠下流的并发调用。Load部分经过LoadManager进行打包办理,其间LoadManager内分红多级LoaderContainer,存在依靠关系的Loader之间由LoaderContainer确保Loader履行前已完结依靠的加载。在LoaderContainer内部一切的Loader均为并行履行,从而削减服务全体延时。为了确保并行操作的安全性,ParallelLoader运用Golang interface接口特性选用双重注册制公职数据流规范。首要各个子Loader需求经过各自LoaderParamer的interface注册将会运用到的Get和Set办法,一起各LoadManager需求注册各业务运用到的Loader,并注册全部的Get及Set办法。
  Pack这类高并发的打包服务一个典型特点是存在很多IRQ,而且小包较多。据架构同学测试,pack单实例(4核4G)OS内部中断超越300k/s。针对这类情况优化分为两部分,首要是由架构同学优化内核,进步处理PPS才能;其次当调用对实时性不高的下流服务时(ie. RtCounter),Pack服务内完结merge组件,将多个恳求汇总后再调用下流服务。

2.5 Data服务简述

  Data服务作为一个朴实CURD服务收敛了一切对根底存储的读写操控不承当业务逻辑,而且只为上层的Post、Data以及Comment-Sort服务运用。一起为了完结读写阻隔及缓存命中率等原因,将Data服务拆分红Post、Default和Offline三个集群,别离承当写入、读取以及离线拉取的职责。
  在存储架构方面LocalCache—Redis—ABase/MySQL 三级存储架构。因MySQL仅存储Meta字段,因此MySQL规划职责可专心于优化谈论列表拉取操作。
  LocalCache运用的是开源的freecache,freecache经过操控目标指针数及分段确保了极小的内存开销和高效的并发拜访才能。
  谈论主存储Redis选用CommentId作为主key,因谈论场景本身的离散性确保了没有热key歪斜的问题(可经过核算各ID切片后方差验证);

对账机

  对账机是独立于谈论三大服务组件之外独立布置的一个监控确保服务,它选用ElasticSearch作为搜索存储引擎。该服务用于完结谈论宣布阶段生命周期监控和海外双机房同步数据一致性监控两大意图,其间海外双机房同步的完结是在此次谈论重构海外对齐阶段完结的。其时项意图背景是MALIVA机房的Musically和ALISG机房的Tiktok两个APP要完结全内容互通,因种种原因谈论没有选用其时通用的底层存储组件DRC同步的计划,而是选择选用上游业务打标回放的计划。谈论组运用异构存储的ElasticSearch对海外双机房抽样进行数据一致性监控,进而确保了MT融合谈论场景的数据安稳性。

三、搬迁计划

  在谈论重构项目中因规划数据模型和存储架构的修正,所以需求谈论团队本身完结包含数据搬迁、服务搬迁在内的搬迁作业。整个过程完结了300亿+数据的导入,以及Post、Pack两个服务的搬迁作业。一起本搬迁计划对上层业务全透明,在搬迁过程中上游业务无任何感知。

3.1 数据搬迁

  数据搬迁分红存量数据搬迁和增量数据追平两部分。谈论选用的计划是存量数据由现有Hive dump 数据至HDFS后经过Spark使命完结新老数据的转化;增量数据追平经过老MySQL binlog日志回放方法完结。在搬迁过程中遇到如下小坑特此记载:

  • 在Spark Quota资源足够的情况下,需求灵活操控写入QPS,否则会打垮下流存储;
  • 因体系Quota办理的问题,会存在使命重启,所以需求业务自己记载搬迁位点,或者经过切块多个子文件并行履行;
  • 因binlog日志默认保存7天,所以需求在7天内完结存量数据导入并开端增量追平流程;
  • 现有Hive表选用增量更新的方法会存在数据丢掉的问题,时间越久丢掉越多;建议搬迁前直接从MySQL dump一份至HDFS;
  • 向新MySQL库导入数据时因冲突问题,先导数据后建索引所用时长会比先建索引快;
  • canal发送binlog日志时会运用主键id作为Partition Key,从而确保了单行有序。假如搬迁脚本同步消费Worker速度不达预期需求经过异步分发提速时,也需求留意经过主键id Hash来确保有序性。
  • 由于binlog日志本身的有序性,所以在binlog日志追平增量数据时不需求记载每个Partition的准确位点,只需求确保Offset提早于存量Hive最新Dump的日期即可;
  • 最后,监控不管多么详细都不过火;

3.2 服务搬迁

  服务搬迁分红Pack服务搬迁和Post服务搬迁两部分。

  Pack搬迁 其间Pack服务搬迁重点是数据验证及逻辑回归,谈论老comment_service服务将上游读恳求除履行业务逻辑外,作为镜像同步调用至新Pack服务。新老服务以LogID为Key将序列化后的Response存入Redis中,由线下Diff脚本读取Redis内数据进行字段校验。
  Post搬迁 Post服务逻辑较为杂乱,大致流程简述如下图所示:

四、相伴而生的轮子

4.1 comments_build_tools东西

  comments_build_tools是一套工程规范和实践规范,能协助开发者快速扩展功用,进步代码质量,进步开发速度。comments_build_tools的主要功用包涵:

  • 针对thriftidl文件里边的每个办法定义生成好用,好扩展且规范的rpc代码。包含四个golang函数以及两个钩子函数;
  • 生成高度可读,且无错的数据库代码,相似于java的jpa或者mybatis,生成ORM到目标的映射;
  • 规范化的测试框架,让单元测试变得简略,解脱服务端单元测试对环境的依靠;
  • 协助你查看代码质量,包含不安全的,或许发生bug的代码;
  • 生成规范高效的枚举代码,枚举供给string、equal等多种办法;
  • 辅助生成泛型库: 详细可参阅下文facility东西库;
  • PS 截止至2019年4月1日该轮子已在GitLab收获67个Star,有任何疑问和需求欢迎加入Lack “comments_build_tools运用群”骚扰轮子作者zhouqian.c;

4.2 facility东西库

  facility库除了上文说到的EventBus、LoadParallel组件外还供给assert句子、类型安全转化以及afunc等功用。
  此外,由于Golang是一种强类型言语,不允许隐式转化;一起Golang1.x 还不支撑泛型;所以在开发过程中言语不如C++、Java精炼。facility 库是运用comments_build_tools 模板编程生成的一个语法糖库,供给各个数据类型的根底功用,包含且不限于如下功用:

  • Set数据结构: 支撑Exist、Add、Remove、Intersect、Union、Minus 等操作
  • Map数据指定默认值;
  • 有序Map: 顺序、逆序、指定序;
  • Slice 数据的查找、类型转化、消重、排序;
  • ?: 三元操作符;

五、结尾

5.1 经验与教训

  1. 周会准则: 谈论重构项目从启动到完结全体搬迁坚持周会准则,而且邀请了多位经验丰富且深刻理解业务的同学一起参与。周会谈论范围包括从架构规划到搬迁计划谈论甚至代码走读,从而确保了计划到细节都经过了充沛的谈论和思辨;
  2. 快速迭代: 在重构过程中快速迭代勇于试错。其时尝试过可是终究未能上线的功用有:服务本身snappy打包、单机Redis进程进行LocalCache办理、根据gossip的LocalCache办理、根据kite client middleware的一致性哈希LoadBlancer etc;
  3. 最大的前向兼容性: 在谈论重构至到服务终究上线,对上游业务方均是透明的;由于谈论服务涉及数十个业务线、上百个上游PSM,假如涉及到上游业务改动,整个排期将不可控;
  4. 对既定方针没有急进的拿结果: 为了确保排期和下降兼容性危险,在计划规划和代码结构上存在退让,导致一些作业拖延到现在重新开端完结;
  5. 对MySQL表规划尤其是索引规划需求慎之又慎,作为业务场景相对固定的表,索引只需求最大极限的确保在线业务需求即可;比方谈论为了或许的回扫场景设置了CreateTime索引,现在此类需求已完全由ElasticSearch完结,所以这个索引现在是废弃状态;
  6. 假如业务中存在某单表读写QPS特别高,能够针对性的选用单库单表的方法,避免一主N从结构扩容时,其它次级表占用存储空间;
  7. 谈论服务存储组件繁复(ABase * 2、MySQL、Redis * N、ElasticSearch),可是对柔性业务确保才能缺乏;

5.2 敞开谈论

  在服务重构过程中有很多剧烈的谈论,本文述而不管罗列出来供我们谈论:

  1. Data服务定位为朴实的CURD层仍是能够承载业务逻辑;
  2. 谈论计数缓存Redis、谈论列表兜底Redis这类轻量级存储组件应该由Data服务履行仍是上抛至Pack服务直接拜访。比方自建于Redis之上的兜底索引的读取是由Pack服务直接读取,仍是下沉至Data服务;
  3. 在代码结构中配置文件的定位是代码既配置仍是配置为文件;