非典型的千万用户后台之路

jopen 9年前

原文  http://guoze.me/2015/12/27/large-scale-server/
 

三年前,原本我只是个不学无术的数据小码农,空有一腔热情;而当时公司也处在艰难的转型期,旧产品不见起色,新产品前途未卜。想见着也不可能用这么小的数据玩出花来,而新产品的数据也不是一时半会能成规模。还是本着最大限度学习的心思,鼓足勇气和老板提换岗,要去扛后台开发的大旗,最大程度参与到产品的一线去。一个小决定,换来的是整整半年的不眠之夜,眼见着第1个用户到第500万个用户,眼见着1台到4台再到10台服务器,眼见着后台业务由单一的播放到能播放能上传再到有完整的社交交互。从刚开始三天两头崩溃出事故,到最终一点不怕市场的同事搞拉新的活动,什么状况都能做到心中有数、遇事不慌。回头一想吓一大跳:自己并不是后台工程师科班出身,从来对语言和框架的争论无感无力,网络编程的基础知识更是差强人意,但是凭着小米步枪,凭着奇技淫巧,凭着持续思考和不断尝试,居然也能搭建起一个支撑千万级别用户的后台框架。总结那半年,留下了5条事关生死的建议,在这里泣血奉上。

数据的读写是服务器性能的核心

一个完整的后台服务,组件其实就只分3种: 接入、逻辑和数据 。这好比一家饭店,后台工程师就是开店的老板,客人数量小于1万,服务流程是第一位的,老板们吭哧吭哧忙着写逻辑;1万到10万之间,接入组件的设计会是重中之重:一个店的服务能力有限,老板们忙着多开几个分店,让客人分流,而决定客人到哪一个分店的,就是接入组件;但是用户一旦大于10万,数据的读写能力就决定了这家超级饭店的服务容量,不管开多少个分店,都要保证数据是一致的,读起来又快又准,而写数据不会影响到读的性能。表结构怎么设计,数据库怎么分布(主从、读写分离、分库、分表),缓存怎么选怎么分布,就是老板们最重要的工作(让老板高兴的是,名片也可以改印个高大上的抬头:架构师)。

一旦用户量过了十万,要再想光靠数据库一部卡车打天下就不太现实了,而缓存(物理存储地在内存,天生比数据库读写性能强)这匹野马的出现就满足了我们对于速度的极致需求。缓存对服务器的架构带来了两个深远的影响:一是热数据和冷数据的分离:热数据访问的人多,缓存挡在前面,为数据库分担巨大的读压力;而热数据从产品的角度也更应获得快速的响应。二是数据一致性的门槛提高,更新数据库的同时必须更新缓存,一旦缓存更新失败,数据库也一定要回滚而保证数据的一致性,不能闹给客人上冷菜的笑话。当然缓存存什么、怎么存,也是大有一番学问,容我下一小节再讲。但缓存的重要性总结一句话: 没有缓存是万万不能的 。无论你是选老马Memcached还是火热的头马Redis,一定要在数据库感受到压力之前上马,并且做好缓存备份和恢复的预案。当然,平安无事你是没办法感受到缓存的好处的,它就像一个平时提醒你吃饭睡觉多喝热水的备胎,只有当她弃你而去之时,你看着服务器哗哗成百倍上涨的响应时间,恨不得找块豆腐一头撞死。

列表、实体和冗余

Web时代,由于翻页前后用户出现了界面的切换,用户对于列表本身的变化并不敏感(假如翻页的同时列表新加入了内容,只要保证用户浏览的这个片段没有重复就可以),但是移动端这种滚动列表的设计简直就是所有后台工程师的梦魇(加入用户上拉列表获取更多的同时新加入了内容,那用户会看到相邻两个重复的内容,然后就气炸了,什么破APP!),应对「列表重复」这个难题的方法出一本书都够了。因为这个需求,我们只能放弃了原有的自增ID,采用时间戳作为获取列表片段的方式:简单来讲,就是客户端每次都上报一个当前页最后一个内容的时间戳,服务器再去取比这个时间更旧的若干个内容。这里必须要感谢 Redis的作者提供了如此丰富的缓存使用的API,我觉得Redis最出色的一点就是把列表的所有使用场景都设想得很通透。

实体就是热数据,热数据的缓存有两问:一是存什么?有人会说简单,把整个结构体转化为一个JSON存进去不就得了?但这其实是有问题的,当你的服务器要面对数十万同时到来的用户,可能短短一瞬就要做数以千万计的JSON到结构体之间的来回切换,而这个过程的效率实际上是很不理想的,那么也许你要想一些更快的方案(此处买个关子)。二是怎么存?雪崩效应并不罕见,一旦源数据改变,一时间许多个线程同时去访问更新缓存的API,服务器瞬间堵死,想到后台工程师会因此而失业,我默默加了一个锁。

小张是端菜的服务员,这次上菜,他要先去凉菜区取个土豆丝、再去荤菜区取个东坡肉、顺到素菜区取个手撕包菜、最后到饮料区再拎两瓶果汁,听起来很低效,对不?这和数据获取的过程是类似的,数据库的表设计首要考虑的是归类,比如用户的信息存一张表,用户和小组的关系再存一张表,那么如果有一个场景需要读用户以及他最后访问过的小组,就得做两次的数据表读取,一旦这个场景频繁出现,适当的数据冗余(把用户最后访问的小组ID加入到用户表的字段中)就能够降低数据库的读取压力。所以 表设计一定一定一定(重要的事情说三遍)要考虑业务场景

异步,是不是真异步?

有的小盆友跑来问我,我这个服务器框架选的牛啊,异步多线程的,单进程并发一万多轻而易举,怎么还是慢啊?我说,「异步」这个词可不要说得太轻松,底层异步了,流程里的每个步骤是不是异步的呢?数据库读写、缓存读写、外部接口的访问,这些都不能异步吧?既然不是异步,卡在哪里你还不知道呢,还不赶紧打日志。还是说说最令我崩溃的一个案例:某次服务器炸了,打多少次日志都没办法定位到卡住的原因。最后猜是怎么着?竟然是日志组件(Log4j)就不是异步的,打日志这个步骤就卡住了,欲哭无泪。

日志、监控和有损服务

一个高级饭店要有厨师,要有大堂经理,要有端盘子的,要有收银的,但千万别忘了还要有保安。他虽然不是饭店成功与否的核心因素,但是如果缺了他,危机时刻就会应付不来。下面这三位哥们就是服务器的保安:日志、监控和有损服务。

先说日志,日志是很微妙的,打多了不行,影响性能、占据空间,打少了,关键问题排查不出原因。那么哪些是必打的呢?我认为有三点:一是行为的基本属性,无非是何时何地何人,时间、用户ID、IP、版本(存下来除了排错,还可以用来做数据分析);二是往返的参数,尤其是客户端上报的参数,服务器返回的数据也许会很大,不建议所有都打印,可以打印统计数据,比如返回了多少个小组之类;三是报错信息,底层一定要catch所有的出错信息,并把它打到单独的日志里。

再说监控,日志是一旦发现了问题帮助我们找出问题的原因的工具,那么什么能帮我们发现问题呢?答案是监控和告警。监控与日志不同,要抓核心的数据,不能多,我建议取三个数据:用户的并发访问数、读取的人均响应时间、写入的人均响应时间,告警的话再加上服务器的崩溃、重启的次数,以及主机性能相关的指标(CPU、内存、硬盘等)。

「发生这种事,大家都不想的。饿不饿,我给你煮碗面?」,服务器运气不好崩溃了,我便常常用这句TVB的经典台词与小伙伴们调侃。其实无论事前机关算尽,成长期的APP总会遇到服务器出状况的。但是,以我有限的经验, 服务器的问题往往不出在自身,而是它所依赖组件导致的问题 ,比如Memcached机器dump、转码服务队列阻塞、或者图片存储空间爆满等等。那么在问题被解决之前,总不能干瞪眼,看着用户投诉一波波来吧?我们会想,对于现在的业务来说,最不能崩溃的场景是什么?比如播放是我们的最基础服务,那我们死也要保证任何外部组件的崩溃都不能影响热门内容的播放,因此我们要把这部分少而重要的热数据加载到内存,以防止外部存储出了什么问题,服务器自己还有碗面吃。真正是,自己的事情自己干,靠天靠地靠祖宗,不算是好汉。

服务分离与复制

服务器体系越长越大,我们首要做的事情是分封,儿子长大了,总要给他一块地盘,当个小王,从此自己打拼去。于是数据读写被抽象成服务了,同时对 APP和前端负责,做最大的一个王;编码解码抽象成服务了,反正编码解码是给UGC用户提供的,想当明星的人总要等得起;日志存储和解析也抽象成服务了,反正有少许的丢失我们也不介意。表面看来服务器被拆得支离破碎,增加了网络时延,是一笔不划算的生意,但实际上对服务器的稳定性大有助益。为什么?一是大王国被拆成小王国了,定位问题更容易,迁移和复制也更简单,数据读写有压力?没问题!再给两块地盘。二是在整个链条上,任何一个环节都是多点,俗话说,不把鸡蛋都放在一个篮子,任何一台服务器dump都不会要了我们的命。

细枝末节且不提,总结当时半年内服务器高速发展期留下来的经验,我认为最重要的就是这五点,业务场景不同,服务器的架构和侧重点也肯定会略有差异;不过这五点基本等同于锦囊,等同于基石,等同于保命符,做好了,这饭店生意一定蒸蒸日上。恭喜你,老板!