如何减少接口响应时间

Posted by 李小武 on March 12, 2016

Premature optimization is the root of all evil.

  — Donald Knuth

对于程序优化,我一直采取保守的态度,除非万不得已。但是随着业务的不断发展,程序越来越复杂,代码越写越多,优化似乎是终有一天会到来的事情。

那么对于一个典型的后台服务接口,我们可以从那些方面入手进行优化呢?

接口拆分

接口垂直拆分

垂直拆分可以简单理解为微服务化,把一个大而复杂的服务拆分成多个相互独立,职能单一的服务,单独部署。 更细粒度拆分的好处是,能对某个具体的微服务进行特殊优化,以最大的投入产出比来解决整个服务的性能。 垂直拆分还有一个好处是,对于非必须的接口,可以很方便的进行降级处理,把坏影响隔离到核心逻辑外部。 最容易想到的优化办法是把某个对整体性能有决定性影响的微服务接口进行水平扩容。

注意: 拆分后必定会增加外部接口调用,多少会有些额外开销,但是对于有限几个调用,拆分的还是值得的。

接口水平拆分

这里说的水平拆分一定不是把一个接口部署更多份,因为这样只能解决接口的容量问题,但是不能减少接口的响应时间。 水平拆分可以简单理解成mapreduce模型,把整个计算逻辑或者数据平均分配到集群中的N个服务器去,然后由一台机器去并发调用并做结果合并。 理论上这种方式能把响应减少到1/N+合并+调用开销的时间。

注意: 一个问题需要考虑的是,如果并发调用的接口返回的数据量比较大,可能会对合并机器的网络负载和数据序列化(CPU)有一定影响。

缓存

接口缓存

一个有着复杂逻辑或者大量计算数据的接口,能对整个结果进行缓存再好不过了。缓存针对不同的场景会有多种策略,对于有大量并发请求的场景, 推荐一个方案:一种基于“哨兵”的分布式缓存设计,不会有损失第一个用户,也不会有定时更新缓存的额外开销。

本地缓存

本地缓存有两种场景,对于类似字典类型的数据,可以静态化后放入内存,定时去刷新或者采用通知机制去更新。

还有一种场景是用ThreadLocal缓存重复内部计算与重复的对象创建; 对于链路比较长或者循环比较深的接口,ThreadLocal减少重复计算和对象创建,从而降低RT和节约内存。

注意: 在有内部并发的地方使用ThreadLocal一定要注意不同线程间的数据同步。主线程的ThreadLocal数据和每个并发子线程的ThreadLocal数据要同步好。

内部优化

非核心流程异步化

类似于发消息,写日志,更新缓存等不会影响接口准确性的非核心流程,可以采用异步方式进行处理,不阻塞主计算逻辑处理。

内部并发

如果进行水平拆分后,并发调用IO较大,可以考虑换成内部并发解决IO问题。如果内部并发涉及到每个线程更新同一个集合数据,不用忘了使用线程安全的集合。 这里有一个并发更新HashMap的case:并发环境下HashMap引起full gc排查

总结

优化一定不是一蹴而就的,整个优化过程是一个统计-->方案-->验证的闭环,需要不断试错,不断挖掘,最终达到预期。