专业 靠谱 的软件外包伙伴

您的位置:首页 > 新闻动态 > Nice图片社交app服务端软件系统架构重构与进化

Nice图片社交app服务端软件系统架构重构与进化

2016-03-14 09:16:01

nice是一款图片社交 App,目标是让人们发现生活的美好。产品的核心体验是基于生活方式的社交。

nice服务端架构重构与演进

我们期望通过图片、直播、标签、潮牌新品等方式,让用户表达自己的生活方式,以这些内容作为基础,为用户提供社交场景。产品方面,目前我们仍然在积极探索怎样更好的为用户提供这种价值。

现阶段,nice 服务端主要面对以下几方面挑战:

  • 系统设计,面向变化,必须能够很好地支撑“产品探索阶段”需求的多样性。

  • 稳定性,避免稳定性问题对现有用户造成伤害,同时还需要应对业务突发性增长。

  • 协作,麻雀虽小五脏俱全,服务端作为客户端、策略推荐、大数据、QA、运营、产品等各团队的桥梁,如何通过技术或非技术手段解决好各方的桥接。

 

“推倒重来”:nice 的重构之路

刚加入 nice,我就接到一个极具挑战性的任务,重构服务端整体业务及框架。和很多创业团队一样,我们在成长过程中积累了一系列技术债务。

  • 旧系统是使用 CI 框架编写的,没有模块的划分,没有明确的分层,入口处直接进行各种业务处理,几乎没有复用。

  • API 的版本管理,是直接目录拷贝,随着业务的发展,已经需要同时维护十多个版本的接口代码,痛苦不堪。

  • 代码中充斥着 if ($isAndroid && $appVersion >= 3) 这样的兼容逻辑。

以至于客户端/服务端联调基本靠喊。我相信初创公司很多朋友都经历过这些问题。

旧系统架构

旧系统架构是一个最典型的一体化的应用架构:后台、 HTML5、接口全部都揉在一起。

nice服务端架构重构与演进

面对当时的状况,首先分析要解决的问题,认为下面 3 个问题比较关键。

  1. 结构性问题。代码结构混乱,无法复用。

  2. 客户端差异管理。接口版本拷贝导致重复代码量巨大,主要针对特定客户端的特殊需求/灰度/小流量等问题。

  3. 客户端/服务端 RD 协作问题。

 

分层和模块化

首先,从大的角度来考虑,可以用简单的两层架构来解决第一个问题。

nice服务端架构重构与演进

应用层和服务层在编码层级进行划分:

  • 应用层解决入口的问题,比如交互协议、 鉴权、 Antispam 等通用服务接入以及各端个性化需求。

  • 服务层解决业务逻辑的问题,将服务层按照业务做垂直的模块划分。

通过层次和模块的划分,代码管理变得清晰,逻辑复用性大大提高。同时,业务划分也为后面的业务隔离和分级管理提供了基础支持。

 

两个基础组件

上述客户端差异管理以及客户端与服务器协作,通过框架的两个基础组件来解决:

nice服务端架构重构与演进

  • ClientAdapter:客户端适配器,用来处理所有客户端差异导致的逻辑问题。

  • CKCR:CheckAndCorrect,数据的检查和修正,用来控制输入输出协议,解决客户端/服务端 RD 协作的技术层面问题。

 

ClientAdapter组件

先来看一个 ClientAdapter 的应用场景。

nice服务端架构重构与演进

以上面配置为例,可以实现以下常用的类似规则。

  • 所有客户端为 3.1.0 及以上版本,支持"打招呼"功能。

  • 所有客户端为 3.1.0 以上版本,灰度渠道 abc36032 为 3.1.0 版本,支持"表情"功能。

nice服务端架构重构与演进

大家看以上 nice 代码,通过这种方法,逻辑上 nice 应对的是各种 "Feature",而不是具体的客户端环境。上面例子就是 ClientAdapter 的一种应用场景。

ClientAdapter 的整体结构

nice服务端架构重构与演进

ClientAdapter 最基础的部分,是抽象出一个”客户端运行时环境”的概念,用它来描述发起每次请求的客户端的各种信息,比如系统、App 版本、IP、网络制式、 网络运营商、 地理位置等。另外,它提供一种简单的描述规则,用来描述一种受限的客户端环境。

应用层面,它仅对外暴露 checkEnv 接口,用来检查当前客户端是否满足给定的描述规则。在这个基础设施之上,nice 上层有很多种应用。

比如原本客户端的差异会导致nice面对复杂的客户端适配,通过 NiceFeature,在 Feature 机制下,RD 面对的其实是一个个产品迭代的 Feature。再比如在 NiceUrl 中,nice 中实现了 CDN 的统一调度,通过 ClientAdapter,我们可以灵活控制各个地区使其采用不同的调度策略。

另外,在处理用户分流方面,这套机制所提供的灵活性也能够满足 nice 按照多种维度进行实验用户抽取的需求。

ClientAdapter 开源地址

ClientAdapter 这个组件实现非常简单,只有 200 行。代码已经摘出放在 github 上了,供大家参考,网址为:http://t.cn/RGnqnpj。

补充一点,ClientAdapter 的设计参考了 C 语言中的常用手段。在 autoconf 阶段,将系统的各种环境信息定义为各种各样的 HAVE_XXX 。这样就达到了环境本身的复杂性和实际的业务代码进行解耦的目的。

 

CKCR 引入

上述问题 3,客户端/服务端 RD 的协作问题,这个问题分两部分。

  • 技术层面:协议层的约束。

  • 流程层面:怎么配合工作。

技术层面上,要解决的问题是怎么让接口协议确保执行。从输入的角度看,我们要不信任客户端,只要协议约定建立就得按规则来,避免客户端被“坏人”控制,对服务造成伤害。从输出的角度看,要将业务层返回的数据确保按照协议约定传输到客户端,避免导致客户端产生不期望的结果,比如常见的类型不 Match 导致 Crash。

一言以蔽之,就是对输入数据的校验和输出数据进行修正。因此,nice 在这里引入了一层叫 CKCR 的组件,全名为 ChecK && CorRect

 

CKCR 实现

CKCR 实现了一套很小的描述性语法规则。通过这套语法来描述要对数据进行的校验和修正。校验和修正行为是可以自由扩展的,下面是它的一个应用示例。

nice服务端架构重构与演进

这个示例中,$data 代表要被处理的数据,$ckcrDesc 就是这个语法规则的描述串。它描述的规则如下。

  • 整体数据是一个 KV 数组(Mapping),只保留 user 和 shows 两个子键的数据。

  • user 也是一个 KV 数组,它的 id 是 int 类型,name 是 str 类型。

  • shows 是一个数组,数组中每个元素是一个 KV 数组,过滤掉它的 id 子键,它的 url 子键,应用 imgCdnUrl 的自定义处理(执行 cdn 调度)。

它和 protobuf、Thrift 的 scheme 有相似之处,也有区别。差异主要在于,CKCR 提供的是一种可扩展的数据校验/修正的通用做法。由于这种扩展性,使得它可以对共性数据,在通用层做更多的文章。比如上面例子中的 imgCdnUrl 就是 nice CDN 调度的统一挂接点。

 

CKCR 内部结构及语法规则

nice服务端架构重构与演进

nice服务端架构重构与演进

上面是 CKCR 的基础功能。后来在应用中发现,在系统各个场景下,系统核心数据输出的数据结构大部分是一样的。因此,CKCR 描述串的复用性就成为了一个问题。

为了解决这个问题,nice 在 CKCR 编译之前,引入了预处理的机制。可以通过特殊的语法,引用固定的数据结构描述。这个机制引入之后,带来了一个附加的好处,即沉淀系统的核心数据结构。

nice服务端架构重构与演进

客户端/服务端协作问题,技术层面 nice 通过这个组件解决。那么,人的协作问题怎么解决?

首先,CKCR 的描述规则是简明的。所以,它可以直接作为接口文档输出。

其次,在接口文档的基础上,nice 服务端和客户端 RD 之间的协作,流程上有一个明确的方案。

  • 定协议:双方 RD 沟通,约定接口协议并提供文档,双方各自进入设计阶段。

  • 假数据:服务端 RD 快速提供 Mock 数据的伪接口,供客户端 RD 基础功能自测使用。

  • 真接口:服务端 RD 提供真实接口,双方联调。

这样分步的开发方式,基本就解决掉了”联调靠喊”的问题。双方的工作基本解耦,并且也基本不影响双方的开发进度。

CKCR 开源地址

CKCR 这个组件的源代码,已经摘出来放在 github 上了,网址为:http://t.cn/RGn57Xk。

第一阶段总结

上面这些就是 nice 应对第一阶段 3 个问题的方案:

  • 分层和模块化:通过两层架构,解决掉结构性问题。

  • 客户端适配器:解决掉客户端差异的问题。

  • CKCR:通过 CKCR 及协作流程,解决掉客户端/服务端 RD 协作的问题。

这个阶段,主要通过整体的重构,解决掉开发的问题,同时为未来的架构调整铺平路。

当时的做法是”推倒重来”,现在回顾,这种选择就当时的情况来说是正确的。在那个时间前后,我们没重构系统所遗留的问题,在系统变得更加庞大之后,变得更加棘手。

不过,话说回来,“推倒重来”的重构之路,毕竟还是充满了各种风险的,做这样的决定前,一定要做好充分的资源和风险评估。

 

为稳定性填的那些坑

在完成整体性的重构之后,nice 进入了业务的快速开发。2015 年 3 月还搞了一个月的 SpeciaForce,研发团队几乎所有人都住公司附近,7 * 14 小时以上的工作量。那段时间的冲刺,为我们的产品带来了日活等关键数据的提升,接口 PV 也达到了最高峰的 5 亿/天。

直到 2015 年 8 月,服务的稳定性经受了很大的考验,说实在的,我当时都快崩溃了,最重要的几类问题如下。

1、MySQL 扛不住

nice 的 MySQL 集群最初就是单实例单库,一主四从,机械硬盘。2015 年 4 月,跟很多业务增长很快的团队一样,为了快速解决问题,OP 把所有的 DB 都换成 SSD,收益非常大。

另外,由于考虑单库导致服务无法隔离,主库的写入也成为瓶颈。2015 年 3 月左右,nice 开始着手考虑分库/分表的事情。分库的方案,主要按垂直的业务进行划分。

技术债真是欠不得!!!

分库整个耗费了两位同学差不多半年的时间。接下来又用了大概一个季度的时间,针对系统的核心大表,进行表的拆分。

在 MySQL 这块儿,我们的教训如下。

  • 用硬件解决问题,性价比非常高。

  • 库的业务线划分,表的规模评估,这样的事情,千万马虎不得。提前做可能多几人天;拖后做可能就像我们需要甚至超过 1 人年的时间去擦屁股。

另外一方面,nice 对 Redis 的依赖也是很重的。一部分数据,是典型的 Cache用法。在线业务访问 Cache 没有的时候,会自动 fallback 到 DB,去冲 Cache。另外一部分数据,是准持久化数据。这部分我们在线业务不会 fallback 到 DB。

2、Redis 扛不住

nice 的 Redis 也是单集群。大概 2015 年四五月的时候,随着业务的快速迭代,很多新功能上线,Redis 的压力迅速增大,开始偶尔出现 Redis 故障。那个时候,nice 就开始了 Redis 的服务拆分。因为业务模式比较简单,所以 Redis的拆分速度是比较快的。

但同时,由于故障中丢过数据,我们决定在 Redis 高可用方面做一些自主开发。主要是针对平滑扩容、 故障自动切换等方面。

由于这方面经验并不是很足,导致出了不小的问题。最严重的一次,上线试运行阶段没有发现问题,但切到全量后,多个集群节点接连出现问题。硬扛了两天左右,最后扛不住,只得往回切换。然而,当时又面临机器资源的问题,做不到一次性全量切回,只能逐集群切换,同时搭上几个 RD 去写几乎所有的准持久化数据的恢复脚本。

Redis 这块儿的教训是非常惨痛的,从 2015 年的踩坑经历来看,我们的收获是如下。

压力与容量评估的经验

  • 压力相关的问题,还是用隔离/拆分的思路解决。

  • 基础的服务监控是必须的,CPU、 内存、 磁盘、带宽等基础资源的监控的实现成本不高,但往往能帮我们提前发现问题。

  • 服务的容量评估,是需要好好思考的。对于在线业务,会 Fallback 到 DB 的 Cache 业务,还要小心故障后穿透的风险。

数据相关的经验

  • 准持久化的数据,如果 Redis 没有做好容灾方案,很好前准备全量恢复数据的备案。不然出问题了现写代码就跪了。

  • 返回首页] [打印] [返回上页]   下一篇