由于一直以来都是做php的, 并且文中涉及的源代码也都是php相关的.
所以就发到php版块了. 如果zuning版主觉得不合适, 请帮忙转移下...
为避免广告嫌疑. 已摘除公司介绍部分.
另外, 分享当时的一些问答, 也没有转过来.
看原文请点下面链接到公众号文章.
去微信公众号阅读原文现阶段,nice 服务端主要面对以下几方面挑战:系统设计,面向变化,必须能够很好地支撑“产品探索阶段”需求的多样性。
稳定性,避免稳定性问题对现有用户造成伤害,同时还需要应对业务突发性增长。
协作,麻雀虽小五脏俱全,服务端作为客户端、策略推荐、大数据、QA、运营、产品等各团队的桥梁,如何通过技术或非技术手段解决好各方的桥接。
“推倒重来”:nice 的重构之路刚加入 nice,我就接到一个极具挑战性的任务,重构服务端整体业务及框架。和很多创业团队一样,我们在成长过程中积累了一系列技术债务。
旧系统是使用 CI 框架编写的,没有模块的划分,没有明确的分层,入口处直接进行各种业务处理,几乎没有复用。
API 的版本管理,是直接目录拷贝,随着业务的发展,已经需要同时维护十多个版本的接口代码,痛苦不堪。
代码中充斥着 if ($isAndroid && $appVersion >= 3) 这样的兼容逻辑。
以至于客户端/服务端联调基本靠喊。我相信初创公司很多朋友都经历过这些问题。
旧系统架构旧系统架构是一个最典型的一体化的应用架构:后台、 HTML5、接口全部都揉在一起。
面对当时的状况,首先分析要解决的问题,认为下面 3 个问题比较关键。
结构性问题。代码结构混乱,无法复用。
客户端差异管理。接口版本拷贝导致重复代码量巨大,主要针对特定客户端的特殊需求/灰度/小流量等问题。
客户端/服务端 RD 协作问题。
分层和模块化首先,从大的角度来考虑,可以用简单的两层架构来解决第一个问题。
应用层和服务层在编码层级进行划分:应用层解决入口的问题,比如交互协议、 鉴权、 Antispam 等通用服务接入以及各端个性化需求。
服务层解决业务逻辑的问题,将服务层按照业务做垂直的模块划分。
通过层次和模块的划分,代码管理变得清晰,逻辑复用性大大提高。同时,业务划分也为后面的业务隔离和分级管理提供了基础支持。
两个基础组件上述客户端差异管理以及客户端与服务器协作,通过框架的两个基础组件来解决:ClientAdapter:客户端适配器,用来处理所有客户端差异导致的逻辑问题。
CKCR:CheckAndCorrect,数据的检查和修正,用来控制输入输出协议,解决客户端/服务端 RD 协作的技术层面问题。
ClientAdapter 组件先来看一个 ClientAdapter 的应用场景。
以上面配置为例,可以实现以下常用的类似规则。
所有客户端为 3.1.0 及以上版本,支持"打招呼"功能。
所有客户端为 3.1.0 以上版本,灰度渠道 abc36032 为 3.1.0 版本,支持"表情"功能。
大家看以上 nice 代码,通过这种方法,逻辑上 nice 应对的是各种 "Feature",而不是具体的客户端环境。上面例子就是 ClientAdapter 的一种应用场景。
ClientAdapter 的整体结构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 实现了一套很小的描述性语法规则。通过这套语法来描述要对数据进行的校验和修正。校验和修正行为是可以自由扩展的,下面是它的一个应用示例。
这个示例中,$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 内部结构及语法规则上面是 CKCR 的基础功能。后来在应用中发现,在系统各个场景下,系统核心数据输出的数据结构大部分是一样的。因此,CKCR 描述串的复用性就成为了一个问题。
为了解决这个问题,nice 在 CKCR 编译之前,引入了预处理的机制。可以通过特殊的语法,引用固定的数据结构描述。这个机制引入之后,带来了一个附加的好处,即沉淀系统的核心数据结构。
客户端/服务端协作问题,技术层面 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 没有做好容灾方案,最好前准备全量恢复数据的备案。不然出问题了现写代码就跪了。
要动线上数据的话一定要准备好回滚方案,不然出问题可能就是灾难。
最后说下自主研发的看法,在技术规模达不到的前提下,建议还是利用已有的成熟方案。即便硬性条件达标,要做这件事,也得每一步都非常小心谨慎。
3、前端机扛不住
最后一个问题,前端机的压力问题。
首先,初创公司规模比较小的时候,接入层一般比较少出问题,不过还是建议做好基本的对“坏人”的防范或预案。
再来看前端机集群,我们出过的问题基本可以归为两类。
客户端的轮询出问题,用户对服务端造成大量请求。
后端服务故障,导致前端机处理慢或者干脆连接不上,这样前端机的进程池很容易就打满了。
因为我们已经对服务的资源进行了划分,业务上也有了模块的划分,所以这件事情的处理就相对容易。大概的思路如下。
前端机根据业务分级。核心业务通常变更较少,其他业务变更导致的故障,避免对核心业务有影响。
降级:我们提供了一套降级预案,出问题能立刻通过上线做降级处理,主要有两类策略。
a. 请求端:应对某个用户群或某个业务出问题;
b. 后端服务:某个依赖的后端服务故障。
后端服务容灾:目前 nice 是增加了一层 LVS 来做后端服务的故障切换(针对 MySQL/Redis 内部服务);另外一点,在前端机应用侧,nice 也做了一层对后端服务的评分机制,避免应用侧才能检测到的问题被忽略掉。
做完上面两个阶段的事情。我们的服务端架构现状大体如下。
nice 现在的痛点和下一步计划2015 年 8 月之后,经过 RD 和 OP 的共同努力,服务稳定性的问题得到了保障。同时技术团队迅速发展,也有了完整的数据和策略团队的同学。在多方对接的过程中,服务化逐渐就变得越来越重要。
另外,自身团队的规模也在扩大,所有的在线业务代码都在一起,有时候上线一次性要上 10 多个人的变更。代码库的耦合也成了一个明显的问题。
目前 nice 面临的最重要的问题就是服务化和代码拆分。
针对这个问题,我们目前的基本思路如下。
服务化,不一刀切。支持远程调用服务的同时,也能够支持现阶段的同机部署。避免服务过多,过早引入服务管理的问题。
代码拆分,应用开发框架升级。将库/框架/业务剥离,独立维护,并引入库的依赖管理工具。
在参与创业的这一年多时间中。我有非常多的感悟。
创业,是一种生活方式。你随时会面临各种各样的问题,有些是你擅长的,有些是你不擅长的。但是无论如何,你都要挺身而出。因为选择了,就要承担。
本次分享以实际问题为主,希望同样参与创业的朋友,看到这次的分享,能少走一点弯路。
回复讨论(解决方案)学习一下,好东西!
完全看不明白支持!!!!
有些看不懂,看来还有很多要学的有点看不懂写的 还是蛮不错的
