Moon's blog

write the code, change the world.

后端一年(经验与心得)

先简单介绍一下我的经历,最早在学校的时候,是在社团里写php和Java,创业时期写js,oc和Ruby,现在是全职用Rails写后端了。

项目简介

我们的主要业务有两块,社区和电商

整体业务的峰值qps大概在3000,也算是pv过10亿的站点了,后端team有4个人,除了一个八年老司机,其他人参加工作的年限都不是太久。

我们面对的是一个巨大的基于Rails的历史遗留系统,最早的开发成员均已离开,导致我们常常面对遗留代码一脸蒙逼,到处是没有人知道的逻辑,丑陋的实现,以及很多性能跟不上的接口。

与巨石应用的斗争

日常工作的重中之重,就是与这个monolith的战斗!

性能篇

以往每年我们搞活动,服务器都会挂,经济损失不少,所以优化性能,保证活动期间的访问是第一要务。

原来的活动整体设计还是比较科学的,活动页面本身是静态化的,主要瓶颈是商品详情页面。

我们利用redis做了三层cache,解决了这个问题。第一层是数据库的缓存,直接把商品信息缓存到redis里,避免了频繁的数据库访问,第二层是单条数据的渲染缓存,可以理解成一小段html,第三层是整个数据集的渲染缓存。

第二个瓶颈出现在一些静态资源上,全面迁移到云存储解决。做完这两件事之后,上上次活动是我们有史以来第一次,没有挂。

就在我们觉得,优化做的不错的时候,上次活动却又挂了。

要知道我们特意买了新服务器,美滋滋觉得这下稳了,没想到…

上次活动挂的原因有以下几点

  1. redis hmget,我们通过gem提供的API,缓存了一个巨大的省市区列表,但是没有注意到缓存是分离的,获取整个列表,其实就是一条hmget获取所有独立的缓存片段,这个操作block了redis,导致访问极度缓慢。我们紧急把整个列表转成json,直接贴到代码里返回hotfix了这个问题
  2. 突然无法通过redis sential进行连接,这套sential系统是由已经离职的运维搭建的,我们绕开sential直接连接redis,解决了这个问题
  3. fd limit,发现依然是1024,修改后却依然时常502,发现运维修改的是root用户的fd数量…坑爹!
  4. 在支付回调中有一段用于统计的sql,订单量大了以后slow query,block了数据库,我们直接注释了这段可有可无的老代码,解决。

总结一下,对于web应用的场景来说,大都是读多写少,缓存读请求,异步写请求,是我们经常采用的两种效果不错的方式。在数据库层面,对于遗留代码中效率低下的查询进行重写,重点改写了所有N+1查询,对一些逐条插入的语句用batch insert合并写入操作,也有不错的提升。

替换篇

做的比较有意思的事,是写了我们内部用的个推GEM。原来使用的是github上开源的一个GEM,但是已经很久没更新了,无法适应我们的使用需求。我基于个推最新的HTTPS的API,写了一个Ruby的包装。

这里要吐槽的是个推的技术水平。推送服务是做的不错,但API怎么做的这么low。他们定义了一个叫authorize的http header用来传递身份信息…违背了RFC关于HTTP头必须大写开头的规范。一些语言的标准库(Go、Ruby…)会自动帮你把authorize转化成Authorize,导致个推那边一直返回auth error…而个推的接口又是HTTPS的,抓包调试很困难,浪费了我很长时间调试这个问题。

重构篇
重构的主要方针就是拆分,尽可能把功能从巨石应用中拆出去。如果一时半会难以拆分的,代码上也尽可能让逻辑高度内聚,方便以后迁移。

消息系统的重构
消息系统是一个,出点问题没什么,但做得好会非常出彩的功能。我一直觉得,像知乎这种社区的成功,除了内容,很大一部分要归功于消息的体验。目前,我们几乎所有页面,都会展示新消息的数量,导致每次请求都会去主数据库的消息表做count,计算各种消息的数量返回给前端。我正在着手把整个系统迁移到另一个独立的数据库,以后可以作为单独的服务供内部调用,降级限流什么的都很方便。

搜索的重构

原来的搜索是基于Solr的java工程,是一个我们内部没人维护好多年的烂摊子,虽然各方面表现都不错。我们使用了用Elasticsearch替换掉了。

新系统
我新写了内部的财务系统,过程中遇到很多问题,写的也很痛苦,但最终效果还是不错。因为原来的各种报表都是直接基于生产数据库的,对业务会有冲击,新系统写了一个同步模块,可以增量同步订单数据到财务系统的专用数据库,这样就不会对业务带来影响。

遇到的比较大的坑就是内存爆炸。有一些耗时计算我放到了消息对列里,整个worker进程的内存占用疯狂上升。最终发现是Ruby内存模型的特点,分配大量对象,却不进行回收。需要你使用batch处理的方式,切成一小块一小块的数据,一次处理一小块,这样下次计算的时候就可以重用之前申请的那些对象。

另外也通过时间换空间的方式,把加载全部数据做计算,改成了加载部分数据做计算,然后汇总结果这样的方式,极大降低了内存占用,并每天重启worker进程,解决了最主要的内存问题(1G内存的机器…)。

这个项目让我真实感觉到,有些场景真的不是Ruby擅长的领域。Ruby的内存模型,就是尽量分配对象,从不真正回收,只会重用。Ruby VM启动就有大量空对象等着被分配,假如我加载了很多数据,空对象不够用了,VM就向操作系统申请一批内存,用完后也不释放,等着下次重用。而报表计算的最佳场景就是能加载大量数据,算一下结果,算完释放掉内存。

监控
可以看我之前的文章使用ELK构建分布式日志分析系统

代码篇
在日常编码、重构的过程中,经常使用的技术是

  1. 设计模式
  2. 元编程
  3. 自动化测试

运用设计模式,写出符合OOP规范的代码。分割每个类的职责,尽量让各个功能的逻辑内聚,只提供彼此间调用的接口,这是我最近才刚领悟的代码整洁之道。

元编程抽象代码,我很早就在使用的奇技淫巧。现在却用的越来越少了,因为它违背了OOP,可维护性比较差,对使用者的水平有很大要求,也容易坑队友

简单地说,我代码中的if/else越来越少了,类越来越多了,改动起来越来方便了,改动影响的部分越来少了,美滋滋。

结语

用一句古老的名言,软件开发没有银弹。