支付网关作为京东支付目前的总入口,在最近一次 618 大促的实战检验中:
承载的峰值用户支付流量 TPS 为 2.2 万以上
承载了期间用户支付的全部流量
经过持续的优化,尤其是支付业务部成立后推动的与间联渠道融合,使整个京东支付在用户层就具备了完整的自我闭环能力,完全解决业内普遍存在依赖单支付机构的瓶颈问题。在日常和大促期间我们的系统定位主要集中在 3 个方面:
一、提供高效稳定的扣款支付能力
二、保障友好化的商城客户支付体验
三、高并发场景下的全时在线服务:
a)业务模块降级
b)基础工具模块热插拔
c)流量分布式平行转移
本次 618 预先启用了 分流和缓冲机制,在保证支付体验的情况下,尽可能的预防了核心业务系统、各支付机构被流量冲击打垮的情况。
支付网关架构和支付业务流程基本简介
1、支付网关的架构模块体系:
按照功能主要分为:业务模块、支付渠道模块、体验保障与高速化模块、基础工具模块。大体的纵向各个功能模块的组织谱系关系如下:
2、支付渠道(以下称为:渠道)主要为支付网关(以下称为:网关)提供了支付工具的扣款能力,网关综合各渠道的扣款能力屏蔽机构间的差异,生成核心支付组件。再结合核心的业务控制能力(风控、路由、商品订单)进行流程化封装生成对支付接口。收银台增添一些先决认证条件:生物识别,手机认证(短信、尾号)、SSO 登录、设备认证通过后调用网关的支付接口进行支付。
3、详细的业务交互时序流程,以其中一个支付流程为例,大体时序为:
根据业务时序图,做过大型企业 ERP、BOSS 系统的人看来并不复杂。但在互联网最大的挑战就是大并发的情况。
4、网关的技术架构思想为:
保障核心业务逻辑稳定或无损平行转移,极端情况的下的 failback
拆解非核心业务逻辑到异步分支流程.
非核心业务逻辑性能差或故障时降级,并实现 failover
取消数据库依赖
基础工具组件双备甚至多备下的热插拔
UUID 并发序列生成器
UUID 是于 2016 年 2 月完全自主研发的业务单号生成系统,对代码进行了开源,完全遵循 LGPL 协议。支付单号生成是我们收单最重要的一步,由支付单号来流转整个支付流程。
它理论基础是按照能有效组织资源的最细粒度拆分服务单元,根据服务单元的属性差异进行唯一化。唯一化的服务单元互相隔离,由于具有唯一性各节点构成分布式,服务单元内部再按照能有效组织的最细粒度资源进行功能克隆拆分子服务单元,共享资源需要被其中一个子服务单元使用是进行排它独占。
实际解决的问题:生产单号多采用数据库、随机数生成,在请求量较少时问题不显著。随着请求量的加大出现重复、卡死的概率逐渐增加且部署数据库、运营成本较高。很多公司的数据库也不光给 uuid 使用,在海量事务处理时是常出现仅为生成 id 就耗费 CPU 时间片,造成正常业务处理延时。
具体技术实现:
我总共写过三种实现:netty、tomcat 做中间件各一版、linuxC 一版。我们目前生产环境中使用 tomcat 版本:
1、注册中心的实现:因为只起到分配实例号的作用。正常情况下 tomcat 实例终身也只在第一次启动时获取一次实例号。由于压力不在生成实例号,简单实现的话使用数据库单表,表的自增主键作为实例号,表中的 md5 值为唯一键。自己也可以生成一个注册中心,只需要注意号记录 md5 和实例号所在文件的文件锁问题即可。生产环境当中我们使用的是 mysql.
2、三种生成 id 的细节性问题:
第一种方式是性能最高最可靠的方式。但由于加入了时间维度,如果在极短的时间内重启完毕,存在单位时间里内存递增变量归 0 递增后重复的概率,于是加入了延时等待的功能让单个实例启动后延时一段时间再提供 id。延时的时间>=时间的维度步长。如:时间维度为 1s 则实例启动后至少应该延时 1s 在提供生成服务。
第二方式:适合 id 号必须连续的场景,比如会计凭证号.但是第二种方式由于没有操作系统文件文件锁的保护,只能当单台机器上只有一个 tomcat 实例的情况下使用。
第三种方式:适合 id 号必须连续的场景,比如会计凭证号。由于有操作系统文件锁保护适合单个机器上存在多个 tomcat 实例的情况使用。
注意:第二、三种方式 linux 系统对同时打开的文件句柄有数量限制,由于序列名跟文件名一一对应,存在文件句柄资源池管理的机制控制文件句柄能最大效率的使用和按需关闭。
3、 有一些公司往往在一台机器上部署多个 tomcat 实例,所以向注册中心注册时使用的是catalina.home. 由于我们用的是 docer 和 jvm 虚拟机,一台虚拟机上只能部署一个 tomcat 不用顾虑这个问题。当然程序进行通用性兼容可以让 PE 们部署的时候放心用。当然这也就为什么会存在第二种方式获取 id 方式的原因。
4、Id 分单个获取和批量获取,批量获取时采用共享变量直接+批量步长的方式,而不是 for 循环。减小 cpu 时间片占用和减少锁长时间占用导致类似 starvation 现象的发生.
5、存在堆 gc 对生成 id 的性能影响,虽然看来非常细微,但是生成 id 是持久化的第一步。它的每延迟增加 1ms 往往带来全链路的延时放大。我们后来找到办法,在大促时段消除了 gc 影响。这个方法我们在后续的技术文章中会专门说明。
6、uuid 的功能比较全,一个 seqName 对应一把线程锁或一把文件锁。
但是业务系统往往只使用一种生成方式和一个 seqName。这样就不能存在多个锁和多个文件句柄有效管理的问题,因此我们业务系统的团队在这个开源代码的基础上进行了裁剪和本地化,能一直保持 seqNmae 对应的文件句柄打开、程序内只存在单个锁供单业务使用,进一步消除了频繁上下文切换的问题。
7、开源代码内部 git 地址:http://source.jd.com/app/uuid.jd.local.git
开源代码内部 svn 地址:http://svn1.360buy-develop.com/buy/Finance/trunk/new_pay/code/uuid
平行迁移
B 平行迁移功能,是我们做故障迁移,流量定位转移的工具。基于 UUID 生成实例号的原理,所有的实例数量都已经存在了注册中立里,区别在于还要把能标记自己的资源定位符也一起给出来.于是注册中心摇身一变成管理端,起到中介者的角色。以一次业务调用为例:
正常情况下:
当实例 1 故障时:
1、 调用端并不是每次都到管理中心拿映射关系,正常情况下调用端只在第一次系统启动时到管理中心获取对端的 URI 并记录到本地,只要对端正常就一直会访问。实际我们调用端有个开关工功能控制出现异常时查询还是每次都查询。
2、 如果每次都查询管理中心,那管理中心的性能如何保证。目前我们采用纯本地 JVM 的 K-V 类型 Map + ReentrantReadWriteLock+数据库 进行解决.
3、 在第 1 条中有说过调用端存在开关机制是异常时查询或每次都查询的开关,我们在进行服务端取余结果跟实例对应关系的时候,如果 NormalCache 赋值的时候以非常小的概率遇到了赋值非原子性操作的问题,无非是两种情况:
一种情况:调用端在利用返回 URI 访问实例的时候出现异常,这个时候调用端会再去访问管理端查询 URI 从而避免。
另一种情况:调用端利用返回的 URI 能正常访问实例,但是我们已经调整映射关系到希望它能访问另外一个实例。其实这个时候场景一般出现在我们密集调整对应关系的时候,这种调整和效果观察的持续时间往往不会短(肯定是秒级以上吧),这个时候我们会打开每次都查询管理中心的开关并持续一段时间,观察访问到了再切回这个开关到异常时查询从而避免。
当然如果你考量这个应用场景还是觉得不放心,那可以在读取的时候用 writeLock 实现.实际由于全部是内存操作、并且数据库读取在获取 Lock 之前,这种情况下采用 Lock 的性能损失接近于无,也非常好。
4、Hash 一致性问题,由于这个实例号的生成逻辑是稳定的,由于是实例号是累增不会中间插入,所以目前不存在 Hash 一致性问题。当然有一些应用场景可能我没遇到,也欢迎大家探讨。
本地化存储
本地存储主要分为两类:
1、一般消息性存储,即把要对外发送的消息出现异常时先存储到本地,然后单独再起线程向原来的接受方进行 传输。由于目前应用场景主要面向消息队列,已经逐渐被我们的部门统一研发的 mqSender 取代,是对消息队列在客户端的 failover 机制的一种扩充。如果要是自己实现的话,单就存储而言在采用 MappedByteBuffer 做内存和刷盘工具+ ReentrantReadWriteLock 进行线程隔离就能满足需求,就不多说了。
2、有顺序保障的结构性存储,是我们进行自行收单的基础下篇会详细讲到。如:同一笔支付需要创建支付单(类似财务的应收概念)、写支付结果(类似财务的实收概念)两个动作.业务上要顺序发生并且必须要用数据库进行持久化存储。问题是创建支付单、写支付结果这两个动作实际流程里因为中间涉及用户交互,延时掉单等问题往往存在支付结果先有,而创建支付单延时的情况或者创建支付单的很久以后支付结果才通过别的方式写入(通过银行发异步接口回调,对账单核对)。
首先交易系统层的小伙伴已经把支付结果和支付单放到不同的表内,做 insert 操作而不是 update。其次是如果入库操作出现异常他们首先也会入缓存,等数据库情况变好后再调度入库。等同一笔支付的支付单、支付结果都存在缓存或数据库时再发起下级非实时业务。
而在支付网关的场景是:调用交易系统出现网络失败,写入延时较高的情况下先断掉与交易系统的交互自行发送保存创建支付单和支付结果到缓存和本地。是由于取消了数据库存储,不使用扫描库的方式。而是使用 java 的 io 事件 selector 进行。通过监听 SelectionKey.OP_READ 事件,根据同一个 payid 到本地文件和缓存内进行条件判断。判断创建支付单、写支付主任务都成功后再发送消息。
缓存双备双切
在京东研发体系内有两个自主研发类似 redis 的缓存系统:JIMDB 和 R2M,我们同时采用。目的是预防其中中一个出现问题能自动或立即切换到另外一个,采用主从异步模式进行互切:
1、 写入时同步写数据到主缓存,异步写数据到从缓存。
2、 读取时采用先从主缓存读取,出现异常和超时再在从缓存中读取。如果主缓存使用写入失败,立即调整主从对应的实际缓存。
其实只需要在 set 和 get 的时候加一个中间层,与 Concurrent 框架里的 Executor 的 newCachedThreadPool(ThreadFactory threadFactory)类似,这个结构和实现比较简单:
未完待续
那上篇就到此结束了,下篇将会重点介绍基于这些技术的上层应用功能:
为取消数据库依赖而使用的自行收单、补单功能。
为增加并发量而使用的异步交互功能。
最重要的为预防支付机构挂掉而使用的路由分流功能。
为保证下级系统进行重构使用的切量平移功能。
以及为保障历次 618,双 11 活动提前进行的保障和洪峰消解工作。
评论