亿级商品数量下,当当这样构建平台化架构
请先看当当的业务特征,这也是绝大多数电商公司的共同特征:
面向互联网海量用户,并且品类繁多。当当作为综合类电商平台,营业范围涵盖图书、日百、服装、母婴等,商品数量是亿级别的。7×24是每个互联网公司的基本要求,流量突增比较容易理解,双11是电商每年的大考,平时各种大促也已渐成常态,5倍以上流量很正常。业务的复杂性,通过这幅简化版的业务架构图说明。
上面部分是面向买家用户的前端系统;下面部分是面向合作伙伴以及运营人员的后端系统;中间是核心业务系统。当当业务核心涵盖了挑选商品 –>下单交易 -> 订单流转 ->库房发货 -> 物流送达的流程。网站由大大小小几百个的系统组成,由于历史原因,系统并不是由单一语言开发。目前主要使用Java和PHP,还有C++和少量遗留的.NET组成。
简单介绍了互联网电商的上下文,我抛出一个问题:互联网架构的核心问题究竟是什么?
如果只总结一个点的话,我的看法是:规模。不断扩大的规模是造成后续诸多问题的根源。
海量用户、商品、促销、订单、支付、仓储、物流等汇集成海量数据。随着PV逐步增加,系统响应也会相应迟缓。串行功能、系统间交互也会导致系统变慢。每个系统都有自己的技术栈,过多的系统导致开发成本难以承受。以前的企业模式,系统部署在几台小型机上,运维难度不大,稳定性较易保障。
当今互联网公司面向水平扩展,更愿意将系统部署在成千上万台廉价PC服务器上。水平扩展提供了伸缩性的同时也对稳定性提出了新的要求。每时每刻都可能有机器挂掉,如果挂掉的机器会对系统造成影响,那么系统的稳定性会急剧下降。
针对上述这些问题,我们使用以下方案解决。
服务化、分片化、异步化和规范化,并且需要在这些解决方案中考虑如何高可用和弹性化。用AOP的概念更加容易解释,饼图内是4个独立的功能模块,高可用和弹性化是他们需要关注的共同逻辑,也就是切面。
为什么需要有服务化的框架?系统拆分后通信方式五花八门,可能通过HTTP、TCP、数据库、消息队列、甚至文件的方式进行交互。若有服务需要下线,如何能够知道哪些消费方仍在调用此服务?没有服务框架的时候,开发工程师需要向整个技术部发邮件询问系统的调用情况,或者根据自行维护的EXCEL判断。如果无人应答或EXCEL更新不同步,服务停掉后才发现有其他系统仍在调用,则悔之晚矣。
服务化框架包括6个核心概念。
服务化框架是为了解决多系统之间调用而产生的框架,透明化远程调用是最基本的需求。最好能够兼容多种RPC和序列化协议。
服务发现用于弹性扩缩容,新上下线的机器,应能通过服务发现自动注册和查找。
负载均衡可根据不同的负载策略分配流量,如轮询,随机或权重等策略。
服务治理用于管理服务依赖和梳理调用关系,也应包含流量控制、服务降级等功能。
监控报警主要是能发现问题,并且将发现的问题通过短信、邮件等方式通知相关处理人。
至于异构语言,前面已经提到过。规模稍大的公司一般不只用单一语言开发,负责各个异构系统交互的服务框架,需要做到支持异构语言。
Dubbo是业界认可度很高的服务框架。
大部分服务化框架应有的功能Dubbo都有。Dubbo是去中心的解决方案,它允许服务的提供者和消费者直接建立连接,而不是将请求通过中心路由节点转发。注册中心只用于分布式协调。
但Dubbo并不是完整的解决方案,有两方面不足。监控中心功能较弱且不具备报警功能,以及对异构语言支持有限。当当在Dubbo的基础上扩展了这两个功能。
监控报警方面,当当使用自主研发的Agent。
Agent将本地收集的调用信息进行SLA计算,以分钟为维度,发送至监控治理中心的Kafka队列。监控治理中心由Storm二次聚合,以时间为维度入库。通过读取数据库内容展现SLA统计和系统关系调用图,并且对接报警接口。
异构语言由DubboX提供。DubboX在Dubbo的基础上扩展了REST协议。
REST协议将RPC字节流转为JSON或XML,以便被其他语言客户端解析。
Dubbo框架通过服务的消费者实现负载均衡,因此使用REST协议的异构语言消费者将失去负载均衡的能力。目前我们由服务提供端使用Nginx做负载均衡的解决方案。Agent独立于DubboX部署,即便是异构语言,也可通过Agent对接监控治理中心。
分片化用于解决海量数据造成的数据库性能问题。很多NoSQL都有自动分片功能,但NoSQL的稳定性、查询灵活性仍然与关系型数据库有差距,NewSQL作为新生事物还并未成熟。所以公司重要的数据仍存放于关系型数据库。数据量超过阀值将导致查询效率下降,水平分片是互联网场景使用关系数据库的常见方式。
当当很早就开始使用数据水平分片的方案,由各业务团队自行实现,基本方案是动态生成SQL字符串或动态选择数据源等,实现成熟度低,可移植性差。随着业务需求越来越复杂,业务和SQL的共同调整常使开发团队焦头烂额。因此我们决定将业务需求与分片技术彻底分离,引入透明化的数据分片中间件。
首先需要明确选择何种方案实现分库分表中间层。我们的技术栈以Java为主,所以仅关注Java的实现方案,主要有以下3种。
当当使用MySQL最多,也有遗留系统仍使用SQLServer。我们希望能够支持尽量多的数据库种类。
ORM框架在当当以MyBatis为主,也有团队使用Hibernate和Spring JDBCTemplate,个别团队直接使用JDBC。多种ORM框架的支持也是需要的。
虽然当当的业务系统主要由Java和PHP开发,但我们更倾向于使用PHP的系统通过调用Java服务来访问需要分库分表的数据库。这里仅支持java可以接受。
性能损耗越低越好。
基于这些方面考虑,我们决定在JDBC层开发。
通过对现有开源数据中间层解决方案的调研,没有发现非常完善的案例,尤其是基于JDBC的案例几乎为零,我们唯有自主研发。我们自研的数据中间层叫Sharding-JDBC。目前关注点是分库分表和事务。
请看Sharding-JDBC分库分表的架构图。
开发者可以认为在操作一个很大的数据库,真实的分片数据全部由框架屏蔽。使用Sharding-JDBC和使用原生JDBC唯一的区别是分片规则配置,之后就完全由框架托管。
框架在JDBC驱动之上进行封装,将原来的DataSource、Connection、Statement或PreparedStatement和ResultSet的一对一关系转换为一对多的集合关系。
获取到待执行SQL后,先将SQL解析为AST语法树,再提取分片关心的部分至解析上下文。
通过SQL改写模块,将原来单机执行的SQL改写为分布式执行的SQL。以分页举例,如果每页展现10条数据,单机数据库获取第2页数据只需获取11到20条,而分布式的场景下需要获取全部的前两页的数据,再到客户端内存统一整合。
路由模块则将SQL替换为实际表名称后,根据配置的分片规则分发至真实的库和表。
如果最终落至多库或表,会由引擎并行执行SQL。
最后将从多个库或表返回的结果数据根据聚合、排序、分组等不同的场景归并。
经过实际测试,Sharding-JDBC在单库场景性能损耗只有0.02%,SQL解析的损耗属可接受范畴。双库并发场景性能提升了94%,证明Sharding-JDBC的分库分表方案可有效提升数据访问性能。
Sharding-JDBC支持关联查询。在SQL兼容性方面也做了很多努力,支持聚合、分组、排序和分页。
XA的分布式事务方案性能低下,互联网公司几乎不会采用。我们倾向使用柔性事务,以牺牲强一致性来换取性能,并通过补偿、修复等机制达到数据的最终一致性。
柔性事务种类很多,我们实现了其中两种,最大努力送达型和TCC型。
最大努力送达型事务的适用场景是数据最终都将成功,不会回滚。实现逻辑比较简单,只要将未成功的SQL反复执行即可,无需业务代码感知。
TCC型事务更加类似于原生事务,提供了Try、Confirm和Cancel流程,但需要使用方自行实现业务代码。
时间关系,只介绍一下最大努力送达型事务的架构图。
事务分为同步送达和异步送达两部分,在执行失败时根据配置的次数进行有限重试。异步送达需要一个稳定的作业组件提供支撑,这里先卖个关子,一会将介绍作业组件。超过了同步和异步最大送达次数的SQL,则需人工干预。柔性事务的本质是将人工干预尽量降至最低。
异步化,主要有两种处理方式,任务调度和消息队列。
任务调度是时间驱动。标准的消息队列是事件驱动,由消息中间件将接收到的消息推送给消息订阅方。任务调度常用于处理批量数据。如:每月与快递公司结算,若本月快递送达超过指定数量将给予额外奖励。消息队列则是一个事件进行一次处理。任务调度常用于系统内部,消息队列可作为多系统解耦的手段之一,系统间通过消息队列交互以完成依赖拆解。间隔时间很短的任务调度也可以作为消息队列的触发引擎,这部分内容后面再介绍。
目前难以找到轻量级的分布式任务调度框架,所以我们仍然采用自主研发。我们的分布式任务调度框架叫Elastic-Job。框架由客户端在到达相应时间点时各自触发调度,并无作业调度中心节点。Elastic-job并不直接提供数据处理功能,而是将分片项分配至各个运行中的作业服务器,开发工程师需要自行处理分片项与真实数据的对应关系。
Elastic-Job能够合理利用分布式资源,动态分配分片项。举例说明:3台服务器,分成10片,则分片项分配结果为服务器A=0,1,2;服务器B=3,4,5;服务器C=6,7,8,9。如果服务器C崩溃,则分片项分配结果为服务器A=0,1,2,3,4;服务器B=5,6,7,8,9。在不丢失分片项的情况下,最大限度的利用现有资源提高吞吐量。同理,增加服务器会有效的疏散分片项,达到系统负载降低的目的。
Elastic-Job由异步事件触发组成,难于在一幅架构图中理清脉络。通过此图主要展示Elastic-Job的各个组成部分,包括注册中心、REST API、运维平台以及基于全量的事件和日志的监控。
消息队列主要分为两类,可靠型和性能型。我各自选择一个代表来分析不足。
ActiveMQ作为实现JMS协议的消息中间件,功能众多。但性能一直是受诟病最多的地方。需要明确业务应用是否真的需要用过高的性能指标来换取不可靠性。根据场景来判断ActiveMQ的性能是否满足会更科学一点。
分片是ActiveMQ一直以来的缺失。Broker和存储端都需要用分片来降低过量的消息和访问带来的负载,这和刚才提到的数据分片化是一样的。
消息和业务数据放在同一本地事务同进同退,是最理想的使用方式。ActiveMQ的事务我画了一个问号,不少人认为ActiveMQ的消息和业务数据放入同一事务需要使用XA,其实不必。ActiveMQ可以和业务数据配置同样的数据源来保证事务的本地性。但可用不代表易用,如果业务数据库本身就是分片化的,ActiveMQ注入多数据源显然需要二次开发。
Kafka作为性能型的消息中间件代表,更多的被用于日志处理等数据不敏感的场景。确实,基于海量吞吐量的处理,某一条消息是否丢失就显得不那么重要了。但Kafka是否如很多人担心的那样,会丢失消息和重复消费?答案是使用得当则可以避免,比如合理配置刷盘策略、消息清理策略和ACK机制等。可靠性的提升必然会带来性能的降低,以性能为代价提升可靠性是否有悖于Kafka的初衷。
消息查询是Kafka的麻烦事,因为消息只有偏移量却并无索引,如果业务方想查看某月某日状态是xx的消息是否全部消费了,需要二次开发。
Kafka使用文件系统而非数据库存储消息,所以与业务数据保持同一本地事务不太现实。
消息队列比数据库中间层要成熟的多,但缺少100%完美的解决方案。当当使用消息队列有自己的权衡方案。
初期使用队列表,通过作业读取存储于数据库中的消息。
后来发展到混合使用,有队列表、ActiveMQ、Kafka、MetaQ、RocketMQ。这些技术栈管理有些杂乱。
未来我们打算使用Elastic-Job来做队列表的触发引擎,用Sharding-JDBC实现队列表的水平扩展,以这两种技术组合出一套更加易用的DMQ。DMQ会封装成为更加易用的接口,支持消息分片和本地事务。这不是第一个组合使用Elastic-Job和Sharding-JDBC的案例,之前提到的柔性事务,作业引擎也是用的Elastic-Job。
规范化提供统一的技术栈解决方案,有效的提升开发效率。
当当对于规范化提出的解决方案是使用统一的应用框架。我们应用框架名字叫dd-frame。
dd-frame定义了一套准入规范和接入标准。中间的核心类包含了可快速启动的容器和基本规范。左边的数据类包含关系型数据库和NoSQL的接入规范。关系数据库复杂一些,需要规范ORM使用,并提供分页组件,代码生成等。右边的集成类主要用于多系统集成,消息队列,调度和服务框架都属于此列。其他类提供了与WEB的整合、单元测试相关和辅助插件。通过dd-frame对接监控生态和集群平台。dd-frame的本质是作为一个胶水,提供便捷整合smart-client的容器。
聊完框架类解决方案,现在可以聊一下当当平台化的理念了。既然要做平台化,不可避免的需要在中心化和去中心化二选一。当当会选用什么方式,听过之前的介绍,相信答案不难推断,我们选择的是去中心化。
上述介绍的几个技术组件,都遵循这个理念,由客户端和注册中心共同协调分布式系统,并未采用中心分发的机制。没有哪种方案是完美的,所以我们需要权衡中心化和去中心化带来的优势和劣势。
去中心化会降低运维难度,分布式逻辑都在类库中,对运维影响小。中心化的中心节点需要有一套额外的集群做支撑,因为所有的流量都要走中心,一旦中心挂了后果很严重。因此运维和部署的复杂度都会增加。
去中心化由客户端直接定位到提供服务的节点,无二次转发,相比于中心化,性能损耗更小。
去中心化的劣势体现在异构语言的支持和资源控制的难度。
因为主要逻辑在客户端,去中心化需要不同语言各自实现一遍,重复开发的工作量很大。DubboX和Elastic-Job虽然支持异构语言,但使用REST协议的DubboX丧失了负载均衡的能力,Elastic-Job也仅能支持shell和http,做不到全语言支持。
集中资源控制也是难题。如Sharding-JDBC的数据库连接管理,Elastic-Job的统一时间触发等问题,均不易解决。
开源给产品本身带来了质量提升和风险降低,对公司技术品牌提升也有极大的推动。我们希望能够从最初的开源社区索取,渐渐转变成给开源社区反馈。
DubboX、Sharding-JDBC和Elastic-Job均已开源,地址分别是:
▽
延展阅读(点击标题):
「聊聊架构」是InfoQ垂直号
在这儿你能得到:
更多互联网架构实践
更多微信群学习机会
手快的已经扫码关注了!