先简单介绍一下我的经历,最早在学校的时候,是在社团里写php和Java,创业时期写js,oc和Ruby,现在是全职用Rails写后端了。
项目简介
我们的主要业务有两块,社区和电商
整体业务的峰值qps大概在3000,也算是pv过10亿的站点了,后端team有4个人,除了一个八年老司机,其他人参加工作的年限都不是太久。
我们面对的是一个巨大的基于Rails的历史遗留系统,最早的开发成员均已离开,导致我们常常面对遗留代码一脸蒙逼,到处是没有人知道的逻辑,丑陋的实现,以及很多性能跟不上的接口。
与巨石应用的斗争
日常工作的重中之重,就是与这个monolith的战斗!
性能篇
以往每年我们搞活动,服务器都会挂,经济损失不少,所以优化性能,保证活动期间的访问是第一要务。
原来的活动整体设计还是比较科学的,活动页面本身是静态化托管到cdn的,从来没有出现过问题,主要瓶颈是商品详情页面。我们利用redis做了三层cache,解决了这个问题。第一层是数据库的缓存,直接把商品信息缓存到redis里,避免了频繁的数据库访问,第二层是单条数据的渲染缓存,可以理解成一小段html,第三层是整个数据集的渲染缓存。第二个瓶颈出现在一些静态资源上,全面迁移到云存储解决。做完这两件事之后,上上次活动是我们有史以来第一次,没有挂。
就在我们觉得,优化做的不错的时候,上次活动却又挂了。
要知道我们特意买了新服务器,美滋滋觉得这下稳了,没想到...
上次活动挂的原因有以下几点
- redis hmget,我们通过gem提供的API,缓存了一个巨大的省市区列表,但是没有注意到缓存是分离的,获取整个列表,其实就是一条hmget获取所有独立的缓存片段,这个操作block了redis,导致访问极度缓慢。我们紧急把整个列表转成json,直接贴到代码里返回hotfix了这个问题
- 突然无法通过redis sential进行连接,这套sential系统是由已经离职的运维搭建的,我们绕开sential直接连接redis,解决了这个问题
- fd limit, 做完以上两点,依然时常502, 发现运维修改的是root用户的fd数量...坑爹....
- 在支付回调中有一段用于统计的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内存模型的问题。
我通过时间换空间的方式,把之前加载全部数据做计算,改成了加载部分数据做计算,然后汇总结果这样的方式,极大降低了内存占用,并通过每天重启worker进程,解决了最主要的内存问题。
这个项目让我真实感觉到,有些场景真的不是Ruby擅长的领域。Ruby的内存模型,就是尽量分配对象,从不真正回收,只会重用。Ruby VM启动就有大量空对象等着被分配,假如我加载了很多数据,空对象不够用了,VM就向操作系统申请一批内存,用完后也不释放,等着下次重用。而报表计算的最佳场景就是能加载大量数据,算一下结果,算完释放掉内存。
监控
可以看我之前的文章使用ELK构建分布式日志分析系统
代码篇
在日常编码、重构的过程中,经常使用的技术是
- 设计模式
- 元编程
运用设计模式,写出符合OOP规范的代码。分割每个类的职责,尽量让各个功能的逻辑内聚,只提供彼此间调用的接口,这是我最近才刚领悟的代码整洁之道。
元编程抽象代码,我很早就在使用的奇技淫巧。现在却用的越来越少了,因为它违背了OOP,可维护性比较差,对使用者的水平有很大要求,也容易坑队友。
简单地说,我代码中的if/else越来越少了,类越来越多了,改动起来越来方便了,改动影响的部分越来少了,美滋滋。
结语
用一句古老的名言,软件开发没有银弹。