大型服务端开发的反模式

1. 用线程池执行异步任务

为了减小阻塞时间,加快响应速度,把无需返回结果的操做变成异步任务,用线程池来执行,这是提升性能的一种手段。
你可能要惊讶了,这么作不对吗? html

首先,咱们把异步任务分为两种:算法

  • 务必成功执行的
  • 不成功就放弃

显然大多数时候都是第一种。那么当你把任务丢给线程池,你知道它完成了没有吗? shell

若是服务器宕机、升级或重启,那些还没有完成或还在排队的任务就丢了。后果是,用户在促销活动中抢到的优惠券,没有发给用户。更严重的后果是,一个订单在送往仓库系统的途中消失了。 数据库

我推荐的作法是把任务投递到消息中间件,让它分发给消息消费者来执行(消费者多是发送者自身)。缓存

  • 消息中间件能够要求消费者在完成任务后通知中间件,不然就从新分发消息,直到收到任务已完成的通知。
  • 若是中间件没这种功能,可让应用要求消费者在完成任务后回发一个"任务已完成"的消息,但应用不能同步等待这一消息,不然异步就退化为同步了。

更重要的是,消息中间件有持久化功能,即便宕机也不丢消息,并且消息中间件能够长期不升级、不重启。消息中间件的缺点是,对失败状况的处理难以定制化——你可能想定制重试间隔、重试次数等细节。 性能优化

换个角度来看,要解决丢任务的问题,你不必定要用消息中间件。你能够在应用代码中把任务和完成状态保存到数据库中,用线程池执行,在完成后更新状态。这是否是很像做业调度(例如Quartz)呢?是的。 服务器

然而,有些任务确实是无关紧要的,例如『刷新一个不重要的缓存』的任务,那么就随便丢到线程池吧。网络

2. 日志采用同步模式

咱们知道,性能瓶颈一般都是I/O,尤为是数据库的I/O。所以咱们用了缓存,速度蹬的一下窜上来了——不必定哦。 运维

用缓存把I/O变成了内存计算,最大瓶颈消除,速度上升一个数量级。在这个数量级,一些本来不重要的因素开始产生影响了。 异步

日志是一种I/O啊,虽然顺序写磁盘很快,但仍是比内存计算要慢啊。更糟的是,一个线程写日志时,另外一个线程必须等它写完才能接着写,不然日志会乱,当日志量较大时,就stop the world了。

因此当你的系统性能高到必定程度,就要对日志作性能优化了(有过提升3倍QPS的案例),两个常见办法:

  • 少打日志
  • 异步模式

今天少打日志,明天排查bug就想哭。因此主要靠异步模式。

Logback能够经过配置(网上搜一搜),在RollingFileAppender上套一个AsyncAppender,实际上就是加了个缓冲队列,变成了批量写磁盘。缺点是没法看到最新日志(还没写磁盘)。queueSize默认是256,即便设为16,也有明显的性能提高,还缓和了不能及时看到最新日志的问题。

Log4j 2的异步模式有更深刻的优化,是否选用,以测试数据为准。

3. 没有超时设置

网络忘记设超时,系统随时可能挂。

每个网络操做,都记得设置超时时间,超过这个时间就放弃。访问分布式缓存也如此。

若是不设超时,可能会等到天荒地老。网络,就是这么不肯定。

4. 没有统计缓存命中率

一个命中率低下的缓存,不如没有。虽然LRU算法很好用,但未必没有例外状况。频繁做废的数据、大致积数据均可能是负担。

统计缓存命中率的实现办法能够是内存里计数,按期写到数据库或文件;也能够是把命中状况打到日志里,往后汇总统计。也可能有更精巧的实现。

当你的系统进入精耕细做时代,这个问题必然摆上案头。

5. 没有统计缓存响应时间

缓存必定快吗?我真的见过不快的。分布式缓存要经由网络,网络抖一抖,缓存抖三抖;还依赖运维,运维抖一抖,缓存抖三抖。此事之微妙,不可不察也。

留个心,设个超时,记个响应时间。一个简单办法是,当响应时间过长时,打一行日志,正常状况不打日志。这样既留了记录,又不产生过多日志。

6. 单一的缓存模式

中央分布式缓存是由缓存中间件组成的集群,一致性较好,缓存的很快会同步到全部副本。可是毕竟要经由网络,延迟为亚毫秒级,并且负载汇集到中间件,可能因网络拥塞或机器负载高而出现性能波动。

为了进一步提升部分业务的性能和稳定性,可能要辅以本地缓存。

  • 缓存要有过时策略,若是使用了Guava Cache,能够选择expireAfterAccess(不经常使用)、expireAfterWrite或refreshAfterWrite策略:

    • expire是先丢弃旧数据,再从数据库加载新数据,期间让数据库承担压力,适用于通常状况。
    • refresh是在异步加载新数据完成前,一直保留旧数据,能始终为数据库挡住压力,适用于高压状况。
  • 各个应用实例的本地缓存是独立的,旧数据的做废依赖于过时策略。做为改进,能够利用消息队列,一个实例广播消息说某数据做废了,其余实例纷纷自检。这是准实时同步。

缓存的更新方式也影响着性能,@左耳朵耗子 的缓存更新的套路很好地介绍了三种套路,如今我来对比一下:

  • Cache Aside Pattern: 应用程序在数据库和缓存之间周旋,以数据库为准。适合通常状况,所以最经常使用。
  • Read/Write Through Pattern: 应用只读写缓存,缓存同步写回数据库(同步是指应用等待着写回完成)。理论性能略高一些。
  • Write Behind Caching Pattern: 应用只读写缓存,缓存异步写回数据库(应用不等待写回完成,缓存若宕机将丢数据)。理论性能最高,若是有Write Ahead Logging(Redis AOF或Apache Ignite均可以)特性,可避免丢数据,但略为下降性能。

7. 分布式缓存加锁

有的系统步入精耕细做时代后,想避免一种状况——缓存做废时,不少应用实例同时访问数据库,加剧负载,并且浪费资源。因而有了给缓存加锁的方案。

简单版是每一个实例内部设锁,某条数据只许一个线程去数据库取。复杂版是把锁设在分布式缓存中,某条数据只许一个实例去数据库取,而后放入缓存让其余实例用。

复杂版的想法是好的,但注意,锁要设置超时(还记得我上文说的吗),不然万一持有锁的实例发生问题,就全体耽误了。即便设了超时,也可能全体实例一直等待超时,浪费时间。因此这种方案不必定好,需以测试数据为准。

8. 疲于奔命

不少公司常常加班,实际上效率低下、也不持久,只能复制既有经验,靠不停换人来维持,其后果就是:需求混乱、bug巨多、创新乏力。

要把技术搞好,须要有条不紊,遇变不乱,持久输出。疲于奔命的模式,作很差大型服务端开发,也难以作好各类领域的开发。

相关文章
相关标签/搜索