这部分内容是我前个礼拜作内部分享的一部分,是挑了大家在日常中经常使用的生产者消费者模式作了一个细节问题的分析来讲述关于系统设计中的一些问题,其实在我前面的救火经验分享里面也有部分的介绍,不过那些比较抽象一点,需要有实际的工作经历的同学才会体会的到,而这里就具体的针对某一个特定场景作了分析.
图 1 生产者消费者模式
生产者和消费者模式在我们日常生产的代码里面应该出现的很频繁,但是有一些细节足以导致这种模式漏洞百出,同时也会使得系统不稳定。在早期Java来实现这种生产者和消费者模式,通常就采用线程池,资源池加上消息通知(wait,notify)的方式来实现,现在的Jdk有原生线程池(ExecutorService)和阻塞式队列,那我就主要说说后者这种实现需要注意的一些问题。
消费者的依赖带来的系统不稳定性
在我们现有系统中消费者往往会依赖于外部系统(文件系统,DB等等),或者内部处理会有比较长时间的消耗,那么对于整个模式来说就会出现在特定情况下队列暴涨(消费者被Hold住,同时控制了总消费者的数量),此时对于队列的压力直接会导致应用系统的不稳定,这时候通常就两种解决方式,一种就是加大消费者线程数(治标不治本),另一种就是将业务处理再细分,同时考虑优化。
业务处理再细分,其实就是考虑对于消费者的角色是否应该还有分层。参看nio等 设计思想,其实可以看到对于工作者角色和消息监听者角色的分割,可以提高对于消息的处理,增加吞吐量。简单来说就是消费者作的事情更少,功能更加单薄,目 的就是将消费这个动作加速,而将具体的业务操作分配给后端的工作线程来做,同时考虑在分配的过程中作合并和其他的优化处理,批量处理消息提高效率。
这么做的优点就在于避免“单点”资源池或者队列的不稳定性,任务在低并发下按常规即时处理,在高并发下批量优化处理。
图2 生产者消费者模式
三个维度解决消费慢于生产的情况
当消费无论如何优化都慢于生产的情况,那么需要考虑在三个维度上去防止异常情况发生。
1. 控制生产者生产频度。
2. 对列或者资源池空间大小限制,同时制定满载的消息处理策略(出错丢弃,磁盘固化等等)
3. 工作者处理时长控制,超时丢弃任务。
资源是宝贵的
这个在以前反复说过,但是实际操作过程中往往被很多同学忽视。
1.使用线程池一定要设置边界,不然连接池不断膨胀会立刻导致内存溢出。
2.队列需要设置大小,特别是在选择时用阻塞队列的时候需要仔细考虑,同时存储在队列的内容尽量是对象的标示,在性能允许的范畴下,由工作线程去获得具体的庞大的处理数据集。
3.超时时间无论如何需要设置,不然依赖的不稳定性随时可以击垮系统。
并行和串行相辅相成
很怕很多同学动不动就起一个线程池,说是多线程效率高。其实是否选择多线程首先就是要考虑这个任务并行执行是否好于串行执行。
1. 是不是关键路径。有时候优化了半天其实到后续还会堵塞在流程的某一阶段,那么多线程的意义就不大了。
2. 会不会有资源竞争。有资源竞争问题不大,但是发现竞争带来的性能损失要远多于多线程带来的性能节省,那么就绝对不选择多线程。并行化计算的最大问题就是一个共享资源访问控制问题,解决这个问题就两种方式:a.共享资源,锁机制保证数据一致性。b.不共享资源,操作结果可合并。(Share nothing,也是MapReduce, Erlang等分布式计算的核心设计理念)
3. 简单的工作串行,复杂的工作多线程并行执行。这个其实回到上面将消费者在分成消息监听者和任务执行者两个角色。(消息监听如果处理得够快,那么采用单线程串行处理也可以接受,只要保证任何异常不会中止监听工作)
多线程,并行处理不是包治百病的良药,串行并行结合起来根据实际场景来合理使用,才会设计出简单高效的系统架构。(设计作复杂容易,作简单难,因此不要在意简单的设计图拿不出手,因为用户只在乎如何得到稳定,高效的服务)
容错策略的抉择
上面有提到如果队列满了应该做一定的策略去保证业务的正常流转。但是对于容错策略的选择上,其实要考虑自己系统地特性。原则如下:
1. 业务需求优先。(任务是否可以丢,任务执行顺序是否有要求,任务的及时性)
2. 架构简单。
3. 不引入新的性能瓶颈和系统不稳定因素。
根据上面的几点,首先业务是否可以丢弃,如果可以丢弃,那么很简单,直接丢弃过载的任务请求(考虑异步记录一些日志备作查询和告警)。如果不可以丢弃,那么就考虑执行的顺序是否有要求,执行的即时性是否有要求,这将直接决定你数据恢复处理的策略。此时就会结合2,3两点来考量方案,有可能会引入持久化操作,同时还有恢复处理的顺序等等。但是一定要仔细判断是否因此会带来其他的性能瓶颈。总结起来一个结论,在业务容许的范围内,结构越简单越好。
后话:
我记得我刚工作那阵子也和现在一些刚毕业不久的同学一样,如果觉得自己的设计很简单,发现出去讲讲都很没面子,一定要想一个很完备的方案,面面俱到,但其实就像我前面所说的,客户在乎的不是你如何实现,而是是否能够满足他的需求(业务上,稳定性,容错性)。
会做出很复杂的设计但是从来不关心客户的想法的程序员仅仅只能算是一个学生。
会考虑如何满足客户需求但是不会走在客户前面多为将来考虑的程序员是一个新手。
会考虑如何满足客户,同时会为客户更进一步思考的程序员是一个合格的程序员。
工作3年 内还是一个新手不可怕,可怕的是工作了几年还是处于一个学生状态,如何把设计从简单做复杂,然后再从复杂作到简单,其实才考验一个人实际的工作能力,在合 时的环境采用合适的方法,得出最简化方案是程序员应该追求的。引用我小时候读书老师常常灌输我的一句话:“书是先要读厚来,然后再读薄的。“