定制网站建设制作,网络运维是干嘛的,商城网站开发技术,建设网站用什么语言好原文#xff1a;http://www.iteye.com/news/31550
-----------
随着乐视硬件抢购的不断升级#xff0c;乐视集团支付面临的请求压力百倍乃至千倍的暴增。作为商品购买的最后一环#xff0c;保证用户快速稳定的完成支付尤为重要。所以在15年11月#xff0c;我们对整个支付…原文http://www.iteye.com/news/31550
-----------
随着乐视硬件抢购的不断升级乐视集团支付面临的请求压力百倍乃至千倍的暴增。作为商品购买的最后一环保证用户快速稳定的完成支付尤为重要。所以在15年11月我们对整个支付系统进行了全面的架构升级使之具备了每秒稳定处理10万订单的能力。为乐视生态各种形式的抢购秒杀活动提供了强有力的支撑。 一、库分表 在redismemcached等缓存系统盛行的互联网时代构建一个支撑每秒十万只读的系统并不复杂无非是通过一致性哈希扩展缓存节点水平扩展web服务器等。支付系统要处理每秒十万笔订单需要的是每秒数十万的数据库更新操作insert加update这在任何一个独立数据库上都是不可能完成的任务所以我们首先要做的是对订单表简称order进行分库与分表。 在进行数据库操作时一般都会有用户ID简称uid字段所以我们选择以uid进行分库分表。 分库策略我们选择了“二叉树分库”所谓“二叉树分库”指的是我们在进行数据库扩容时都是以2的倍数进行扩容。比如1台扩容到2台2台扩容到4台4台扩容到8台以此类推。这种分库方式的好处是我们在进行扩容时只需DBA进行表级的数据同步而不需要自己写脚本进行行级数据同步。 光是有分库是不够的经过持续压力测试我们发现在同一数据库中对多个表进行并发更新的效率要远远大于对一个表进行并发更新所以我们在每个分库中都将order表拆分成10份order_0order_1….order_9。 最后我们把order表放在了8个分库中编号1到8分别对应DB1到DB8每个分库中10个分表编号0到9分别对应order_0到order_9部署结构如下图所示 根据uid计算数据库编号 数据库编号 (uid / 10) % 8 1 根据uid计算表编号 表编号 uid % 10 当uid9527时根据上面的算法其实是把uid分成了两部分952和7其中952模8加1等于1为数据库编号而7则为表编号。所以uid9527的订单信息需要去DB1库中的order_7表查找。具体算法流程也可参见下图 有了分库分表的结构与算法最后就是寻找分库分表的实现工具目前市面上约有两种类型的分库分表工具 1.客户端分库分表在客户端完成分库分表操作直连数据库 2.使用分库分表中间件客户端连分库分表中间件由中间件完成分库分表操作 这两种类型的工具市面上都有这里不一一列举总的来看这两类工具各有利弊。客户端分库分表由于直连数据库所以性能比使用分库分表中间件高15%到20%。而使用分库分表中间件由于进行了统一的中间件管理将分库分表操作和客户端隔离模块划分更加清晰便于DBA进行统一管理。 我们选择的是在客户端分库分表因为我们自己开发并开源了一套数据层访问框架它的代号叫“芒果”芒果框架原生支持分库分表功能并且配置起来非常简单。 芒果主页mango.jfaster.org芒果源码github.com/jfaster/mango 二、订单ID订单系统的ID必须具有全局唯一的特征最简单的方式是利用数据库的序列每操作一次就能获得一个全局唯一的自增ID如果要支持每秒处理10万订单那每秒将至少需要生成10万个订单ID通过数据库生成自增ID显然无法完成上述要求。所以我们只能通过内存计算获得全局唯一的订单ID。 JAVA领域最著名的唯一ID应该算是UUID了不过UUID太长而且包含字母不适合作为订单ID。通过反复比较与筛选我们借鉴了Twitter的Snowflake算法实现了全局唯一ID。下面是订单ID的简化结构图 上图分为3个部分 时间戳 这里时间戳的粒度是毫秒级生成订单ID时使用System.currentTimeMillis()作为时间戳。 机器号 每个订单服务器都将被分配一个唯一的编号生成订单ID时直接使用该唯一编号作为机器号即可。 自增序号 当在同一服务器的同一毫秒中有多个生成订单ID的请求时会在当前毫秒下自增此序号下一个毫秒此序号继续从0开始。比如在同一服务器同一毫秒有3个生成订单ID的请求这3个订单ID的自增序号部分将分别是012。 上面3个部分组合我们就能快速生成全局唯一的订单ID。不过光全局唯一还不够很多时候我们会只根据订单ID直接查询订单信息这时由于没有uid我们不知道去哪个分库的分表中查询遍历所有的库的所有表这显然不行。所以我们需要将分库分表的信息添加到订单ID上下面是带分库分表信息的订单ID简化结构图 我们在生成的全局订单ID头部添加了分库与分表的信息这样只根据订单ID我们也能快速的查询到对应的订单信息。 分库分表信息具体包含哪些内容第一部分有讨论到我们将订单表按uid维度拆分成了8个数据库每个数据库10张表最简单的分库分表信息只需一个长度为2的字符串即可存储第1位存数据库编号取值范围1到8第2位存表编号取值范围0到9。 还是按照第一部分根据uid计算数据库编号和表编号的算法当uid9527时分库信息1分表信息7将他们进行组合两位的分库分表信息即为”17”。具体算法流程参见下图 上述使用表编号作为分表信息没有任何问题但使用数据库编号作为分库信息却存在隐患考虑未来的扩容需求我们需要将8库扩容到16库这时取值范围1到8的分库信息将无法支撑1到16的分库场景分库路由将无法正确完成我们将上诉问题简称为分库信息精度丢失。 为解决分库信息精度丢失问题我们需要对分库信息精度进行冗余即我们现在保存的分库信息要支持以后的扩容。这里我们假设最终我们会扩容到64台数据库所以新的分库信息算法为 分库信息 (uid / 10) % 64 1 当uid9527时根据新的算法分库信息57这里的57并不是真正数据库的编号它冗余了最后扩展到64台数据库的分库信息精度。我们当前只有8台数据库实际数据库编号还需根据下面的公式进行计算 实际数据库编号 (分库信息 - 1) % 8 1 当uid9527时分库信息57实际数据库编号1分库分表信息”577”。 由于我们选择模64来保存精度冗余后的分库信息保存分库信息的长度由1变为了2最后的分库分表信息的长度为3。具体算法流程也可参见下图 如上图所示在计算分库信息的时候采用了模64的方式冗余了分库信息精度这样当我们的系统以后需要扩容到16库32库64库都不会再有问题。 上面的订单ID结构已经能很好的满足我们当前与之后的扩容需求但考虑到业务的不确定性我们在订单ID的最前方加了1位用于标识订单ID的版本这个版本号属于冗余数据目前并没有用到。下面是最终订单ID简化结构图 Snowflake算法github.com/twitter/snowflake 三、最终一致性到目前为止我们通过对order表uid维度的分库分表实现了order表的超高并发写入与更新并能通过uid和订单ID查询订单信息。但作为一个开放的集团支付系统我们还需要通过业务线ID又称商户ID简称bid来查询订单信息所以我们引入了bid维度的order表集群将uid维度的order表集群冗余一份到bid维度的order表集群中要根据bid查询订单信息时只需查bid维度的order表集群即可。 上面的方案虽然简单但保持两个order表集群的数据一致性是一件很麻烦的事情。两个表集群显然是在不同的数据库集群中如果在写入与更新中引入强一致性的分布式事务这无疑会大大降低系统效率增长服务响应时间这是我们所不能接受的所以我们引入了消息队列进行异步数据同步来实现数据的最终一致性。当然消息队列的各种异常也会造成数据不一致所以我们又引入了实时监控服务实时计算两个集群的数据差异并进行一致性同步。 下面是简化的一致性同步图 四、数据库高可用没有任何机器或服务能保证在线上稳定运行不出故障。比如某一时间某一数据库主库宕机这时我们将不能对该库进行读写操作线上服务将受到影响。 所谓数据库高可用指的是当数据库由于各种原因出现问题时能实时或快速的恢复数据库服务并修补数据从整个集群的角度看就像没有出任何问题一样。需要注意的是这里的恢复数据库服务并不一定是指修复原有数据库也包括将服务切换到另外备用的数据库。 数据库高可用的主要工作是数据库恢复与数据修补一般我们以完成这两项工作的时间长短作为衡量高可用好坏的标准。这里有一个恶性循环的问题数据库恢复的时间越长不一致数据越多数据修补的时间就会越长整体修复的时间就会变得更长。所以数据库的快速恢复成了数据库高可用的重中之重试想一下如果我们能在数据库出故障的1秒之内完成数据库恢复修复不一致的数据和成本也会大大降低。 下图是一个最经典的主从结构 上图中有1台web服务器和3台数据库其中DB1是主库DB2和DB3是从库。我们在这里假设web服务器由项目组维护而数据库服务器由DBA维护。 当从库DB2出现问题时DBA会通知项目组项目组将DB2从web服务的配置列表中删除重启web服务器这样出错的节点DB2将不再被访问整个数据库服务得到恢复等DBA修复DB2时再由项目组将DB2添加到web服务。 当主库DB1出现问题时DBA会将DB2切换为主库并通知项目组项目组使用DB2替换原有的主库DB1重启web服务器这样web服务将使用新的主库DB2而DB1将不再被访问整个数据库服务得到恢复等DBA修复DB1时再将DB1作为DB2的从库即可。 上面的经典结构有很大的弊病不管主库或从库出现问题都需要DBA和项目组协同完成数据库服务恢复这很难做到自动化而且恢复工程也过于缓慢。 我们认为数据库运维应该和项目组分开当数据库出现问题时应由DBA实现统一恢复不需要项目组操作服务这样便于做到自动化缩短服务恢复时间。 先来看从库高可用结构图 如上图所示web服务器将不再直接连接主库DB1而是连接KeepAlive虚拟出的一个虚拟ip再将此虚拟ip映射到主库DB1上同时添加DB_bak从库实时同步DB1中的数据。正常情况下web还是在DB1中读写数据当DB1宕机后脚本会自动将DB_bak设置成主库并将虚拟ip映射到DB_bak上web服务将使用健康的DB_bak作为主库进行读写访问。这样只需几秒的时间就能完成主数据库服务恢复。 组合上面的结构得到主从高可用结构图 数据库高可用还包含数据修补由于我们在操作核心数据时都是先记录日志再执行更新加上实现了近乎实时的快速恢复数据库服务所以修补的数据量都不大一个简单的恢复脚本就能快速完成数据修复。 五、数据分级支付系统除了最核心的支付订单表与支付流水表外还有一些配置信息表和一些用户相关信息表。如果所有的读操作都在数据库上完成系统性能将大打折扣所以我们引入了数据分级机制。 我们简单的将支付系统的数据划分成了3级 第1级订单数据和支付流水数据这两块数据对实时性和精确性要求很高所以不添加任何缓存读写操作将直接操作数据库。 第2级用户相关数据这些数据和用户相关具有读多写少的特征所以我们使用redis进行缓存。 第3级支付配置信息这些数据和用户无关具有数据量小频繁读几乎不修改的特征所以我们使用本地内存进行缓存。 使用本地内存缓存有一个数据同步问题因为配置信息缓存在内存中而本地内存无法感知到配置信息在数据库的修改这样会造成数据库中数据和本地内存中数据不一致的问题。 为了解决此问题我们开发了一个高可用的消息推送平台当配置信息被修改时我们可以使用推送平台给支付系统所有的服务器推送配置文件更新消息服务器收到消息会自动更新配置信息并给出成功反馈。 六、粗细管道黑客攻击前端重试等一些原因会造成请求量的暴涨如果我们的服务被激增的请求给一波打死想要重新恢复就是一件非常痛苦和繁琐的过程。 举个简单的例子我们目前订单的处理能力是平均10万下单每秒峰值14万下单每秒如果同一秒钟有100万个下单请求进入支付系统毫无疑问我们的整个支付系统就会崩溃后续源源不断的请求会让我们的服务集群根本启动不起来唯一的办法只能是切断所有流量重启整个集群再慢慢导入流量。 我们在对外的web服务器上加一层“粗细管道”就能很好的解决上面的问题。 下面是粗细管道简单的结构图 请看上面的结构图http请求在进入web集群前会先经过一层粗细管道。入口端是粗口我们设置最大能支持100万请求每秒多余的请求会被直接抛弃掉。出口端是细口我们设置给web集群10万请求每秒。剩余的90万请求会在粗细管道中排队等待web集群处理完老的请求后才会有新的请求从管道中出来给web集群处理。这样web集群处理的请求数每秒永远不会超过10万在这个负载下集群中的各个服务都会高校运转整个集群也不会因为暴增的请求而停止服务。 如何实现粗细管道nginx商业版中已经有了支持相关资料请搜索 nginx max_conns需要注意的是max_conns是活跃连接数具体设置除了需要确定最大TPS外还需确定平均响应时间。 nginx相关
http://nginx.org/en/docs/http/ngx_http_upstream_module.html