订单导出应对大流量订单导出时的设计问题
背景
2018年4月16日,订单导出跪了。几乎接近于崩溃,导出接口响应非常慢,以至于前端直接报错。最后只能通过重启服务器解决。
事后排查发现: 当时有多个VIP商家在线上导出,有 X 个大流量导出,都在几十万的订单量导出之间,导出只有m台机器,当时访问Hbase集群大量超时,线程被hang住,最终无力支撑。虽然在预发验证过 100w+ 万订单的导出,可是没预料到多个稍小但也很大的导出同时“侵袭”导出服务器。 自此,开始了“应对VIP大流量导出”的设计优化旅程。
整体设计
这里需要说明下大流量订单导出的整体设计,采用了“批量并发”的处理策略。
-
由于导出订单量可能非常大,“全部加载到内存,计算报表字段逻辑,再写入文件”的方式很容易把内存撑爆,因此,采用了批次的概念。将全部要导出的订单量,切分成多个批次,每批次处理 Batch 条订单,批量写入指定文件; 批次之间是串行的;
-
为了保证整体性能,当发现要导出订单数大于 Threshold 时,就会通过并发策略来导出。上面的每批次 Batch 条订单,会被切分成 num 个订单,多个线程并发去获取订单详情数据,然后聚合起来进行格式化计算,写入文件。
-
订单导出拉取订单详情先通过订单详情批量API接口,然后要访问多个Hbase表,其中有一个大量级表的一次scanByPrefix和一个稍小量级表的2次scanByPrefix,四个大量级表的多次batchGet,以及一些稍小量级的batchGet。大量级表和小量级表的差别也就至多一个数量级的差别。
-
合法者不拒。 只要是合法的导出请求,都会开启线程去处理,没有限制。
求解之旅
限制与隔离
显然,如果多个大流量订单导出同时来到,假设一个 X 订单的导出。那么就需要 X/num 次详情接口的并发调用, 4X/num 次大量级表的多次batchGet,X/num次大量级表的 scanByPrefix 和 2X/num 次小量级表的 scanByPrefix, N*X/num次小量级表的batchGet。 假设有 10X 订单量的同时导出,上述每个数字乘以 10, 然后还要在比较短的时间内完成,可见,1000个线程也不够用啊!
因此,大流量订单的合法导出即使不拒绝,也不能立即响应。 这里需要作出一些限制。 比如每次同时只能导出 Y 个VIP大流量导出。但这个策略也是很粗略的。 更优化的策略可以是: 专门设置一个大流量订单导出队列, 检测到大流量导出后先放到该队列,在机器相对空闲的时候再进行处理。
最彻底的方案是隔离大流量导出。系统稳定性,实质上是资源分配和使用的问题。保证系统稳定性,可以从两个方面来保证: 1. 保证合理的资源分配和利用;2. 隔离不稳定性源,保证整体不受影响。合理的资源利用,包括资源的限速、限流、线程池和连接池的优化; 隔离不稳定性源,即是将可能导致不稳定性的因素隔离在正常服务之外,即使部分出现不稳定,整体也不受影响。
之所以大流量导出会影响整体导出服务稳定性,正是因为不稳定性源混杂在正常服务中,未做有效隔离。 大流量导出与正常流量导出混杂在一起,大流量导出会占用大量线程,导致正常流量导出资源分配不够,从而影响整体服务。 因此,有必要将大流量导出抽离出来,用专门的服务器来完成。这样,正常流量导出始终是稳定的,而大流量导出即使有问题,也只会影响极少数导出,不会影响整体导出服务的稳定性。且可以重试。
识别关键威胁
要真正提升系统稳定性,就要识别出关键威胁并解决它。对于订单导出来说,有个大量级表的 scanByPrefix 容易超时,消耗大量服务器资源并带来很大压力。之所以用 scan, 这个是历史设计问题。 当时导出整体架构设计从DB迁移到大数据中心来获取数据,整体方案是可行的,但没有意识到这个设计会给大流量导出以及多个大流量导出下的导出系统稳定性埋下隐患。现在看来,干掉 scan ,尽可能采用 batchGet 来获取数据,然后在客户端来聚合数据,是一个更好的策略。 遗憾的是,当时没有做太多思考,等意识到这个设计问题时,有点为时过晚。 参阅: 两个设计教训:前瞻性思考与根本性解决方案 。
Hbase表的访问优化
当时故障的直接原因是,scanByPrefix 大量超时,导致线程被hang住。跟数据组同学讨论后,认为这个操作太耗Hbase集群服务器资源。因此,做了三个优化:
-
Hbase超时设定。 Hbase默认的超时设定值比较大,导致线程长久被hang住无法被释放。 合理的超时设置,不一定能避免应用崩溃,但不仔细的超时设置,在应用出现问题征兆时会放大问题;举一反三,对于应用中所用到的默认设置,都应该仔细斟酌下,量身定制。
-
加 Hbase 主备切换, 如果主集群访问超时,自动切换到备集群访问,减少主集群压力。
-
scan 在服务端, filter by prefix 移到客户端去过滤。 可以很大程度上减少Hbase集群的压力。做过这个优化后,超时基本没有了。不过也带来了新的问题:导出客户端压力过大。这种做法导致导出应用在scan和filter大流量订单时,CPU和内存都大幅攀升。因为导出需要获取(可能远)超出所需的数据,然后过滤出指定的订单号列表的数据。这说明,有些优化会解决面临的问题,但是会引入新的问题。尽管如此,解决超时仍然是向正确方向迈进了一步,保证Hbase集群的整体稳定性高于单个导出服务器的稳定性。
-
同一个表的多次访问进行合并。
减少不必要的访问
另一个方法是梳理和优化详情流程,减少不必要的访问。 新报表或老报表的导出不需要某些字段,就可以不用访问某些Hbase表,减少访问Hbase表的IO流量;此外,只获取需要的字段,也可以减少服务间的传输消耗。如有可能,都应该指定要获取的列集合,避免暴力性获取所有字段的数据。
线程池独立和监控
在故障发生后,发现导出任务提交和订单详情数据拉取共用一个导出任务提交线程池,这样也是有隐患的。因此增加了两个线程池: 批量调用详情接口的线程池和并发获取Hbase数据的线程池,并进行线程池的监控。
小结
一些初始设计在常见场景下并不存在问题,但是在大压力场景下会给系统稳定性带来隐患,这一点日后切要注意。 另外,做系统局部优化时,也要全局考虑,避免因为优化某个局部又引入了新的甚至威胁更大的问题。