立足互联网行业,架构通常指的是技术架构,更具体一点的说是系统架构、软件架构,或者是最常见的网站架构。本期我们共同探讨2017年互联网时代,实战型技术架构的演进过程及其优缺点等诸多方面。
文/张成远
关于数据库的使用,在京东有几个趋势,早期主要用 SQL Server 及 Oracle 也有少量采用 MySQL,考虑到业务发展技术积累及使用成本等因素,很多业务都开始使用 MySQL,包括早期使用 SQL Server 及 Oracle 的很多核心业务也都渐渐开始迁移到 MySQL,单机 MySQL 往往无法支撑这类业务,需要考虑分布式解决方案,另外原本使用 MySQL 的业务随着数据量及访问量的增加也会遇到瓶颈,最终考虑采用分布式解决方案,整个京东随着业务发展采用数据库的趋势如图1所示。
图1 业务使用数据库演变趋势
分布式数据库解决方案有很多种,在各个互联网公司也是非常普遍,本质上就是将数据拆开存储在多个节点上从而缓解单节点的压力,业务层面也可以根据业务特点自行进行拆分,如图2所示,假设有一张 user 表,以 ID 为拆分键,假设拆分成两份,最简单的就是奇数 ID 的数据落到一个存储节点上,偶数 ID 的数据落到另外一个存储节点上,实际部署示意图如图3所示。
图2 数据拆分示意图
图3 系统部署示意图
除了业务层面做拆分,也可以考虑采用较为通用的一些解决方案,主要分为两类,一类是客户端解决方案,这种方案是在业务应用中引入特定的客户端包,通过该客户端包完成数据的拆分查询及结果汇总等操作,这种方案对业务有一定侵入性,随着业务应用实例部署的数量越来越多,数据库端可能会面临连接数据库压力也越来越大的问题,另外版本升级也比较困难,优点是链路较短,从应用实例直接到数据库。
另一类是中间件的解决方案,这种方案是提供兼容数据库传输协议及语法规范的代理,业务在连接中间件的时候可以直接使用传统的 JDBC 等客户端,从而大大减轻业务开发层面的负担,弊端是中间件的开发难度会比客户端方案稍微高一点,另外网络传输链路上多走了一段,理论上对性能略有影响,实际使用环境中这些系统都是在机房内网访问,这种网络上的影响完全可以忽略不计。
根据上述分析,为了更好得支撑京东大量的大规模数据量业务,我们开发了一套兼容 MySQL 协议的分布式数据库的中间件解决方案,称之为 JProxy,这套方案经过了多次演变最终完成并支撑了京东全集团的去 Oracle/SQL Server 任务。
JProxy 第一个版本如图4所示,每个 JProxy 都会有一个配置文件,我们会在其中配置相应业务的库表拆分信息及路由信息,JProxy 接收到 SQL 以后会对 SQL 进行解析再根据路由信息决定 SQL 是否需要重写及该发往哪些节点,等各节点结果返回以后再将结果汇总按照 MySQL 传输协议返回给应用。
图4 JProxy 版本一
结合上文的例子,当用户查询 user 这张表时假设 SQL 语句是 select * from user where id = 1 or id = 2,当收到这条 SQL 以后,JProxy 会将 SQL 拆分为 select * from user where id=1 及 select * from user where id = 2, 再分别把这两条 SQL 语句发往后端的节点上,最后将两个节点上获取到的两条记录一并返回给应用。
这种方案在业务库表比较少的时候是可行的,随着业务的发展,库表的数量可能会不断增加,尤其是针对去 Oracle 的业务在切换数据库的时候可能是一次切换几张表,下一次再切换另外几张表,这就要求经常修改配置文件。另外 JProxy 在部署的时候至少需要两份甚至多份,如图5所示,此时面临一个问题是如何保证所有的配置文件在不断修改的过程中是完全一致的。在早期运维过程中,我们靠人工修改完一份配置文件,再将相应的配置文件拷贝给其他 JProxy,确保 JProxy 配置文件内容一致,这个过程心智负担较重且容易出错。
图5 配置文件
在之后的版本中我们引入了 JManager 模块,这个模块负责的工作是管理配置文件中的路由元信息,如图6所示。JProxy 的路由信息都是到JManager统一获取,我们只需要通过 JManager 往元数据库里添加修改路由元数据,操作完成以后通知各个 JProxy 动态加载路由信息就可以保证每个 JProxy 的路由信息是完全一致的,从而解决维护路由元信息一致性的痛点。
图6 JProxy 版本二
在提到分布式数据库解决方案时一定会考虑的一个问题是扩容,扩容有两种方式,一种我们称之为 Re-sharding 方案,简单的说就是一片拆两片,两片拆为四片,如图7所示,原本只有一个 MySQL 实例一个 shard,之后拆分成 shard1 和 shard2 两个分片,之后再添加新的 MySQL 实例,将 shard1 拆分成 shard11 和 shard12 两个分片,将 shard2 拆分成 shard21 和 shard22 两个分片放到另外新加的 MySQL 实例上,这种扩容方式是最理想的,但具体实现的时候会略微麻烦一点,我们短期之内选择了另一种偏保守一点、在合理预估前提下足以支撑业务发展的扩容模式,我们称之为 Pre-sharding 方案,这种方案是预先拆分在一定时期内足够用的分片数,在前期数据量较少时这些分片可以放在一个或少量的几个 MySQL 实例上,等后期数据量增大以后可以往集群中加新的 MySQL 实例,将原本的分片迁移到新添加的 MySQL 实例上,如图8所示,我们在一开始就拆分成了 shard1、 shard2、 shard3、 shard4 四个分片,这四个分片最初是在一个 MySQL 实例上,数据量增大以后我们可以添加新的 MySQL 实例,将 shard3 和 shard4 迁移新的 MySQL 实例上,整个集群分片数没有发生变化但是容量已经变成了原来的两倍。
图7 Pre-sharding 方案
Pre-sharding方案
Pre-sharding方案相当于通过迁移实现扩容的目的,分片位置的变动涉及到数据的迁移验证及路由元数据的变更等一系列变动,所以我们引入了 JTransfer 系统,如图9所示。JTransfer 可以做到在线无缝迁移,迁移扩容时只需提交一条迁移计划,指定将某个分片从哪个源实例迁移到哪个目标实例,可以指定在何时开始迁移任务,等到了时间点系统会自动开始。整个过程中涉及到基础全量数据和迁移过程中业务访问产生的增量数据。一开始会将基础全量数据从源实例中 dump 出来到目标实例恢复,验证数据正确以后开始追赶增量数据,当增量数据追赶到一定程度,系统预估可以快速追赶结束时,我们会做一个短暂的锁定操作,从而确保将最后的增量全部追赶完成。这个锁定时间也是在提交迁移任务时可以指定的一个参数,比如最多只能锁定 20s。如果因为此时访问量突然增大等原因最终剩余的增量没能在 20s 内追赶完成,整个迁移任务将会放弃,确保对线上访问影响达到最小。迁移完成之后会将路由元信息进行修改,同时将路由元信息推送给所有的 JProxy,最后再解除锁定,访问将根据路由打到分片所在的新位置。
图9 JProxy 版本三
系统在生产环境中使用的时候,除了考虑以上的介绍以外还需要考虑很多部署及运维的事情,首先要考虑的就是系统如何活下来,需要考虑系统的自我保护能力,要确保系统的稳定性,要做到性能能够满足业务需求。
在 JProxy 内部我们采用了基于事件驱动的网络 I/O 模型,同时考虑到多核等特点,将整个系统的性能发挥到极致,在压测时 JProxy 表现出来的性能随着 MySQL 实例的增加几乎是呈现线性增长的趋势,而且整个过程中 JProxy 所在机器毫无压力。
保证性能还不够,还需要考虑控制连接数、控制系统内存等,连接数主要是控制连接的数量。这个比较好理解,控制内存主要是指控制系统在使用过程中对内存的需求量,比如在做数据抽取时,SQL 语句是类似 select * from table 这种的全量查询,此时后端所有的 MySQL 数据会通过多条连接并发地往中间件发送数据,从中间件到应用只有一条连接,如果不对内存进行控制就会造成中间件 OOM。在具体实现的时候,我们通过将数据压在 TCP 栈中来控制中间件前后端连接的网络流速,从而很好的保证了整个系统的内存是在可控范围内。
另外还需要考虑权限,哪些 IP 可以访问,哪些 IP 不能访问都需要可以精确控制。具体到某一张表还需要控制增删改查的权限,我们建议业务在写 SQL 的时候尽量都带有拆分字段,保证 SQL 都可以落在某个分片上从而保证整个访问是足够的简单可控,我们为之提供了精细的权限控制,可以做到表级别的增删改查权限,包括是否要带有拆分字段,最大程度做到对 SQL 的控制,保证业务在测试阶段写出不满足期望的 SQL 都能及时发现,大大降低后期线上运行时的风险。
除了基本的稳定性之外,在整个系统全局上还需要考虑到服务高可用方案。JProxy 是无状态的,一个业务在同一个机房内部署至少两个 JProxy 且必须跨机架,保证在同一个机房里 JProxy 是高可用的。在另外的机房会再部署两个 JProxy,做到跨机房的高可用。除了中间件自身的高可用以外,还需要保证数据库层面的高可用,全链路的高可用才是真正的高可用。数据库层面在同一个机房里会按照一主一从部署,在备用机房会再部署一个备份,如图10所示。JProxy 访问 MySQL 时通过域名访问,如果 MySQL 的主出异常,数据库会进行相应的主从切换操作,JProxy可以访问到切换以后新的主。如果整个机房的数据库异常可以直接将数据的域名切换到备用机房,保证 JProxy 可以访问到备用机房的数据库。业务访问 JProxy 时也是通过域名访问,如果一个机房的 JProxy 都出现了异常,和数据库类似,直接将 JProxy 前端的域名切换到备用机房,从而保证业务始终都能正常访问 JProxy。
图10 部署示意图
数据高可靠也是非常关键的点,我们会针对数据进行定期备份到相应的存储系统中,从而保证数据库中的数据即使被删除依然可以恢复。
系统在线上运行时监控报警极其重要。监控可以分多个层次,如图11所示,从主机和操作系统的信息到应用系统的信息到系统内部特定信息的监控等。针对操作系统及主机的监控,京东有 MJDOS 系统可以把系统的内存/CPU/磁/网卡/机器负载等各种信息都纳入监控系统,这些操作系统的基础信息对系统异常的诊断非常关键,比如因为网络丢包等引起的服务异常都可以在这个监控系统中及时找到根源。
图11 监控体系
京东还有统一的监控报警系统 UMP,这个监控系统主要是为所有的应用系统服务。所有的应用系统按照一定的规则暴露接口,在 UMP 系统中注册以后,UMP 系统就可以提供一整套监控报警服务,最基本的比如系统的存活监控以及是否有慢查询等。
除了这两个基本的监控系统以外,我们还针对整套中间件系统开发了定制的监控系统 JMonitor。之所以开发这套监控系统是因为我们需要采集更多的定制的监控信息,在系统发生异常时能够第一时间定位问题。举个例子,当业务发现 TP99 下降时往往伴随着有慢 SQL,应用从发送 SQL 到收到结果这个过程中经过了 JProxy 到 MySQL 又从 MySQL 经过 JProxy 再回到应用,这条链路上任何一个环节都可能慢,不管是哪个阶段耗时,我们需要将这种慢 SQL 的记录精细化,精细到各个阶段都花了多少时间,做到出现慢 SQL 时能快速准确的找到问题根源并快速解决问题。
另外在配合业务去 Oracle/SQL Server 时,我们不建议使用跨库的事务,但是会出现有一种情况,同一个事务里的 SQL 都是带有拆分字段的,每条 SQL 都是单节点的,同一个事务里有多条这种 SQL,结果却出现这个事务是跨库的,这种事务我们都会有详细的记录,业务方可以直接通过 JMonitor 找到这种事务从而更好的进一步改进。除了这个以外,业务系统最初写的 SQL 没有考虑太多的优化可能会出现比较多的慢 SQL,这些慢 SQL 我们都会统一采集在 JMonitor 系统上进行分析处理,帮助业务方快速迭代调整 SQL 语句。
业务在使用这套系统的时候要尽量出现避免跨库的 SQL,有一个很重要的原因是当出现跨库 SQL 时会耗费 MySQL 较多的连接,如图12所示。一条不带拆分字段的 SQL 将会发送到所有的分片上,如果在一个 MySQL 实例上有64个分片,那一条这样的 SQL 就会耗费这个 MySQL 实例上的64个连接,这个资源消耗是非常可观的,如果可以控制 SQL 落在单个分片上可以大大降低 MySQL 实例上的连接压力。
图12 连接数
跨库的分布式事务要尽量避免,一个是基于 MySQL 的分布式数据库中间件的方案无法保证严格的分布式事务语义,另一个即使可以做到严格的分布式事务语义支持依然要尽量避免垮库事务。多个跨库的分布式事务在某个分片上发生死锁将会造成其他分片上的事务也无法继续,从而导致大面积的死锁,即使是单节点上的事务也要尽量控制事务小一点,降低死锁发生的概率。
具体路由策略不同的业务可以特殊对待。以京东分拣中心为例,各个分拣中心的大小差异很大,北京上海等大城市的分拣中心数据量很大,其他城市的分拣中心相对会小一点,针对这种特点我们会给其定制路由策略,做到将大的分拣中心的数据落在特定的性能较好的 MySQL 实例上,其他小的分拣中心的数据可以按照普通的拆分方式处理。
在 JProxy 系统层面我们可以支持多租户模式,但考虑到去 Oracle/SQL Server 的业务往往都是非常重要且数据量巨大的业务,所以我们的系统都是不同的业务独立部署一套,在部署层面避免各个业务之间的互相影响。考虑到独立部署会造成一些资源浪费,我们引入了容器系统,将操作系统资源通过容器的方式进行隔离,从而保证系统资源的充分利用。很多问题没必要一定要在代码层面解决,代码层面解决起来比较麻烦或者不能做到百分之百把控的事情可以通过架构层面来解决,架构层面不好解决的事情可以通过部署的层面来解决,部署层面不好解决的事情可以通过产品层面来解决,解决问题的方式各式各样,需要从整个系统全局角度来综合考量,不管黑猫白猫,能抓老鼠的就是好猫,同样的道理,能支撑住业务发展的系统就是好系统。
另外再简单讨论一下为什么基于 MySQL 的分布式数据库中间件系统无法保证严格的分布式事务语义。所谓分布式事务语义本质上就是事务的语义,包含了 ACID 属性,分别是原子性、一致性、持久性、隔离性。
原子性是指一个事务要么成功要么失败,不能存在中间状态。持久性是指一个事务一旦提交成功那么要做到系统崩溃以后再恢复依然是成功的。隔离性是指各个并发事务之间是隔离的,不可见的,在数据库具体实现上可能会分很多个隔离级别。事务的一致性是指要保证系统要处于一个一致的状态,比如从 A 账户转了500元到 B 账户,那么从整体系统来看系统的总金额是没有发生变化的,不能出现 A 的账户已经减去500元但是 B 账户却没有增加500元的情况。
图13 可串行化调度
事务在数据库系统中执行的时候有一个可串行化调度的问题。假设有 T1、T2、T3 三个事务,那么这三个事务的执行效果应该和三个事务串行执行效果一样,也就是最终效果应该是{T1/T2/T3, T1/T3/T2, T2/T1/T3, T2/T3/T1, T3/T1/T2, T3/T2/T1}集合中的一个。当涉及到分布式事务时,每个子事务之间的调度要和全局的分布式事务的调度顺序一致才能满足可串行化调度的要求,如图13所示,T1/T2/T3 的三个分布式事务,在一个库中的调度顺序是 T1/T2/T3 和全局的调度顺序一致,在另一个库中的调度顺序变成了 T3/T2/T1,此时站在全局的角度来看就打破了可串行化调度,可串行化调度保证了隔离性的实现,当可串行化调度被打破时自然隔离性也就随之打破。在基于 MySQL 的分布式中间件方案实现上,因为同一个分布式事务的各个子事务的事务 ID 是在各个 MySQL 上生成的,并没有提供全局的事务 ID 来保证各个子事务的调度顺序和全局的分布式事务一致,导致隔离性是无法保证的,所以说当前基于 MySQL 的分布式事务是无法保证严格的分布式事务语义支持的。当然随着 MySQL 引入 GR 可以做到 CAP 理论中的强一致,再加强中间件的相关功能及定制 MySQL 相关功能,也是有可能做到支持严格的分布式事务的。
文/廖超超
互联网研发,唯快不破。为了提升公司整体研发效率,百度引入了业界的优秀工程实践,设计开发了一整套研发工具链。主要包括项目管理平台、代码开发协作平台和持续交付平台,分别针对需求、开发和交付场景,提供工具、流程和数据支持,如图1所示。
图1 百度研发工具链
代码管理的目标场景就是开发场景,是研发活动的核心环节,承载着打通需求、交付上下游的作用。百度代码管理建设分别从文化传播、工程实践和产品建设三个方面入手推进公司代码管理水平的不断提升。为此,我们推出了代码管理建设的五级金字塔模型,如图2所示,分别代表了代码管理建设的不同能力水平。
图2 百度代码管理概况
最底层是代码托管,这是代码管理最基础的能力。
第二层是协同开发,就是支持各个业务线在不同的研发模式下进行快而有序地协作开发。百度的产品、业务线众多,不同的团队规模、不同的开发语言、不同的研发模式都给开发协同提出了不同的需求。
第三层是 DevOps 支持,实现产品全生命周期的工具全链路打通与自动化。
第四层通过研发数据度量体系的建设,给公司提供研发数据参考,促进研发流程的改进。
第五层工程师文化建设,在公司内部推行代码评审、内部开源、社交化编程等工程师文化。
百度拥有万人开发团队,近十万项目,每周代码自动检出的问题超二十万,每天发起评审超1万次。为了保证代码质量,我们要求代码提交前和提交后都进行自动化检查。为了加速编译和集成,我们有大规模的分布式编译系统和持续集成系统。百度 C/C++ 语言是源码依赖,编译系统需要检出所有的依赖代码,这样代码库的访问压力呈指数级增长。这些都是百度代码管理面临的挑战,总结起来就是这三点:代码质量、规模协同和安全稳定的服务,如图3所示。
图3 百度代码管理遇到的挑战
面对这三大挑战,代码开发协作平台重点解决代码管理五个方面的问题:代码托管、协同开发、代码质量、代码安全与开放、研发改进。
代码托管是研发的基础设施。代码托管需要保证服务的安全、稳定和可靠,同时保证在大规模协同场景下的高性能。
基于代码入库流程,提供简单易用的代码评审,并且在评审环节支持代码扫描、编码规范、安全扫描等自动化检查,同时支持打通持续集成进行自动化测试,从而保证代码入库前就得到充分的质量检验。
代码安全要求对访问控制权限做严格的限制,需要支持安全扫描和安全审计等;代码开放鼓励代码共享、开源,从而实现代码复用。
支持主流的 Workflow 以满足各业务线不同的研发模式的需求,如:传统的分支开发、主干开发、特性分支、git flow 等工作流。
研发管理需要有数据支撑,用数据度量一切,不断地优化研发流程,促进高效协同,提升研发效率。
代码开发协作平台经历了这四个阶段的演进。面对不同阶段的不同问题,我们所采用的方案也不同,如图4所示。
图4 百度代码管理架构演进的历程
为了快速验证产品,我们采用了精益的思路快速实现了 MVP。在代码库服务设计上,暂时不考虑容量和性能的问题,采用了 Master-Slave 这种单实例结构,如图5所示。
图5 产品初创时期——架构
在这种架构下,我们主要从两个方面保证服务的安全可靠。
随着平台的快速发展,代码库的并发和容量都在急剧增长。我们首先采用大内存、SSD 硬盘来提升硬件性能。然后进行 I/O、网络、缓存等优化。经过反复的性能测试得到了单机的最优配置。
在扩容方案上我们主要考虑了两种方案:分布式存储和数据分片。
我们经过性能测试和 MVP 验证后,最终选择了数据分片的方案。主要的原因就是代码服务是高 I/O 的服务,分布式存储的 I/O 性能较本地存储差距比较明显,尤其写的性能更是下降了一个数量级。
图6所示的这版架构的主要改变是 git 服务分实例部署。根据 Repositories 进行分片,将不同的 Repositories 分配到不同的实例。数据库服务采用主备的方式独立部署,同时支持了数据库访问的读写分离。
图6 产品发展时期——架构
用户请求首先经过 proxy,调用统一的路由服务将请求转发到对应实例。认证服务独立部署,proxy 集成认证模块,加强了用户身份认证。
路由服务是核心服务。为了降低业务系统的改造成本,设计了统一的路由服务和路由模块,通过切面的方式拦截所有对代码库的访问请求,从而实现对业务代码的较低侵入和对调用方的透明。在路由设计上,因为首先使用了去中心化的微服务架构,所以采用客户端路由的方式。同时,增加了本地缓存,即使路由服务宕机,路由依然可以正常运行,如图7所示。
图7 产品发展时期——路由设计
由于编译、自动化测试、持续集成等需求出现了爆发式的增长,代码库每日读的请求超过30万次,每日写的请求超2万次。高峰时段,TPS 将近1000,千兆网卡全部被打满。经过对吞吐量的需求的评估,我们预计 TPS 将突破10000。为了保证性能,高峰时段下载代码的速率,自动化系统应该在 30MB/s 以上,开发人员必须在 5MB/s 以上。因此,吞吐量不足的问题已经成为最核心的问题。我们的改进方案如下:
图8是读写分离的架构图,通过 proxy 判断读写请求,将写请求发给 master 节点,读请求通过负载均衡模块分别发给实例的所有节点。在这版架构升级的过程中,我们还是采用 DRBD+KeepAlived 实现容灾备份方案。读写分离大大提高了系统吞吐量,但是 DRBD 冷备机器闲置,严重浪费资源。所以,我们做了进一步的改进。
图8 产品成熟期——架构
图9是改进后的架构图,我们废弃了 DRBD 备份,并且实现自己的高可用方案。我们的方案主要分为两个阶段:
图9 产品成熟期——架构优化
百度代码开发协作平台整体上采用微服务架构,基于自主研发的微服务框架构建各个业务服务单元,独立开发、发布、部署和运行。整体的架构如图10所示。
图10 百度代码开发协作平台架构
Httpd Proxy 主要用于 Web 访问,Sshd Proxy 用于 Git 命令行操作,API Gateway 用于统一提供开放 API,便于 API 的安全授权、管理。
在接入服务之上采用百度统一前端接入服务构建高可用负载均衡器,一方面提高系统的并发访问能力,另一方面提高系统的防攻击能力,保证平台的安全性。
平台构建统一的安全策略和用户认证体系,确保系统安全。
服务中心是服务治理的核心,提供服务注册/发现、服务路由、服务配置、服务降级、服务熔断、流量控制等功能,保证平台整体的服务稳定性。通过服务路由、配置管理中心、服务注册/发现等机制来统一管理服务,另外提供统一的管理控制台管理应用服务集群、Git 集群和基础服务集群等。
平台同时支持 Webhook 和 Plugin 两种开放能力的方案,支持第三方系统方便集成。Webhook 主要的应用场景是当开发人员提交代码变更后,自动触发持续集成构建。Plugin 主要应用在代码评审环节的自动代码检查。
业务服务通过微服务架构组织服务单元,每一个业务服务都会注册到服务中心,在调用其他业务服务时也是通过服务中心的服务发现机制去获取某一特定服务的具体提供实例列表,通过客户端路由方式来决定具体调用哪个服务提供者,从而既保证服务可靠性,又能提高系统吞吐量。平台提供代码管理、代码浏览、代码评审、代码搜索、代码扫描等业务组件。
Git 集群是平台的最核心、最基础的部分。为了保证 Git 集群的安全、高可用和高性能,平台提供了如下能力:
1) 同时支持数据的软实时和硬实时备份能力。
2) 提供三重备份,每一份代码至少有三份拷贝。
3) 提供根据不同的分片策略对数据分片的能力,支持 Git 集群动态扩容。
4) 提供读写分离的能力,支持一主多备。
5) 提供 HA 方案,支持自动 failover。
平台依赖数据库、索引、缓存、用户管理、通知、存储等多个基础服务。这些基础服务在保障自身可用性的同时,提高了平台整体可靠性。
经过一系列的架构改进,代码开发协作平台的容量、性能、可靠性都已经得到提升和验证。随着百度效率云产品对外服务,代码管理在其他方面遇到了更大的挑战。
结合以上所述企业级 SaaS 对代码管理的要求,我们提出了企业专属云方案,如图11所示。
图11 专属云
我们主要从三个方面实现企业专属云:
围绕着企业专属云方案,我们在架构设计上加强了多租户的管理、资源的管理,如图12所示。
图12 企业级 SaaS 服务下代码管理架构
在多租户管理方面,在统一的访问控制层增加了租户认证,所有请求都需要带上租户信息才可以通过认证。
在资源管理方面,同时支持 Docker 资源和虚拟机资源。企业接入时,Admin 系统将自动从统一的资源池申请资源,通过 Docker 的方式完成自动化部署。我们同时支持混部和独立部署的方式,混部就是在同一个资源上部署多个企业的代码库实例。对于对代码安全有更高隔离要求的客户,我们将他们的服务独立部署在一台虚拟机上。
百度代码开发协作平台使用微服务架构构建业务服务,一方面整合了现有的业务系统,另一方面提高了系统的稳定性和性能。使用数据分片和读写分离相结合的方式解决了代码库服务容量和性能的问题。使用专属云方案处理多租户的问题,帮助企业客户快速接入,实现资源隔离。但是,我们还有很多不足的地方有待提高和完善。比如,目前我们考虑到性能和开发成本的问题,选择了数据分片来扩容。但是,随着代码库容量的不断提升,数据分片带来的架构复杂、运维成本、性能瓶颈等问题也开始显现出来。读写分离和主备切换的方案,在高并发读的场景下工作尚可,但是面对高并发写的场景性能和可靠性就难以满足。
架构设计是和业务需求紧密相关的,只有合适的架构才是好的架构,因此,产品发展的不同阶段需要选择不同的技术架构方案。同时,一种可演进的架构是应对业务需求发展和变化的较优选择。
文/陈俊超
本文详细描述了 PhxSQL 的设计与实现。从 MySQL 的容灾缺陷开始讲起,接着阐述实现高可用强一致的思路,然后具体分析每个实现环节要注意的要点和解决方案,最后展示了 PhxSQL 在容灾和性能上的成果。
互联网应用中账号和金融类关键系统要求和强调强一致性及高可用性。当面临机器损坏、网络分区、主备手工或者自动切换时,传统的 MySQL 主备难以保证强一致性和高可用性。PhxSQL 将 MySQL 集群构建在一致性完善的 Paxos 协议基础上,保证了集群内 MySQL 机器之间数据的强一致性和整个集群的高可用性。
MySQL 有两种常见的复制方案,异步复制和半同步复制。
Master 对数据进行 commit 操作后再将数据异步复制到 Slave。
但数据无法保证成功复制,也就无法保证 MySQL 主备间的数据一致性,如图1所示。
图1 MySQL 异步复制流程
Master 对数据进行 commit 操作前将数据复制到 Slave,确认复制成功后再对数据进行 commit 操作。
绝大多数情况下,半同步复制能保证 MySQL 主备间的数据一致性,如图2所示。
图2 MySQL 半同步复制流程
半同步方案中的“半”是指 Master 在等待 Slave 的 ACK 失败时将退化成异步复制。同时,MySQL 在重启时也不会执行半同步复制。
如图3中的 id(Gtid)=101 数据是 Master 机器中新写入到 Binlog File 的 Binlog 数据。但 Master 在复制数据到 Slave 的过程中 MySQL 宕机导致复制失败。MySQL 重启时,数据(id=101)会被直接进行 commit 操作,随后再将数据异步复制到Slave。(下文将已经写入到 Binlog File 但未进行 commit 操作的数据(id=101)称为 Pending Binlog。)
图3 MySQL 重启时直接提交 Pending Binlog
该情况下 MySQL 容易出现 Master-Slave 之间数据不一致的情况,官方也描述了该问题。
下面将解释 MySQL 在重启时不执行半同步会产生数据不一致的原因。
当对上述例子中的 Pending Binlog(id=101)进行复制时 Master 宕机导致复制失败,随后 Slave1 切换成新 Master 并开始提供服务(写入id=201的数据)。此后,当旧 Master 重启时,Pending Binlog(id=101)不会被重新进行复制而直接进行 commit 操作,从而导致旧 Master 比新 Master 多了一条数据,旧 Master 无法成为新 Master 的 Slave,需要人工处理掉这条数据之后,才能让旧 Master 作为 Slave 提供服务,如图4所示。
图4 MySQL 重启缺陷导致主备数据不一致
上述 case 只对旧 Master 的数据造成影响,不会使得 MySQL Client 读取到错误数据。但当 Master 连续出现两次宕机后产生 Master 切换,两次宕机间隔较短使得 Pending Binlog 未能及时复制到 Slave,且期间有查询请求时(Master 宕机→ Master 重启→查询数据→ Master 宕机→ Master 切换),MySQL Client 会产生如图5所示的幻读(两次读到的结果不一致)。
图5 MySQL 重启缺陷导致 Client 产生幻读
当 Master 出现故障且产生 Master 切换时,由于原生 MySQL 缺乏调用端的通知/重定向机制,使得不同的Client可能访问不同的 Master ,导致数据的错误写入和读取,如图6所示。
由于半同步复制不需要等待所有 Slave 的 ACK,因此当 Master 出现故障时,需要选有最新 Binlog 的 Slave 为新的 Master ;而 MySQL 并没有内置这个选主机制,如图7所示。
图7 MySQL 缺少自动选主机制
MySQL 在容灾方面存在的问题:1. Master 切换时主备数据不能保证一致:Master 重启并切换可能导致 MySQL 主备间数据不一致。Master 重启并切换可能导致 MySQL Client 产生幻读。2. 原生 MySQL 缺乏高可用机制:Master 切换导致调用端分裂。缺乏自动选主机制。
对于原生 MySQL ,在高可用和强一致两个特性中,只能二选一:1. 要求 MySQL 主备间的数据强一致,不做主备自动切换。2. 借助 MHA 实现高可用,容忍 MySQL 主备间的数据不一致。
因此 MySQL 在容灾上无法同时满足数据强一致和服务高可用两个特性。
实现一个以可靠日志存储为中心的架构来解决 MySQL 数据复制时产生的数据不一致问题。
Master 将 Binlog 发送到 BinlogSvr 集群(可靠日志存储),Slave 从 BinlogSvr 集群获取 Binlog 数据完成数据复制。
Master 在重启时,根据 BinlogSvr 集群的数据判断 Pending Binlog 是否已经被复制。如果未被复制则从 Binlog File 中删除。
利用 BinlogSvr 集群(可靠日志存储),使得 Master (重启时检查本地 Binlog 是否和 BinlogSvr 集群的数据一致)和 Slave(从 BinlogSvr 集群中获取 Binlog)的数据保持一致,从而保证了整个集群中的 MySQL 主备间数据的一致性,如图8所示。
图8 实现一个可靠日志存储保证各 MySQL 的数据一致
在 Master 进行切换时,切换操作可能会导致部分 MySQL Client仍然访问旧 Master 并读到旧数据。最直观的方法是修改 MySQL Client API,在每一次进行查询时,先确认当前 Master 的位置。但此方法有以下缺点:
为了避免修改 MySQL Client API,可通过增加 Proxy 进行请求透传来解决上述问题。在每一个 MySQL 结点上增加一个 Proxy,MySQL Client 的请求不再直接访问 MySQL 而直接访问 Proxy。Proxy 根据 Master 的位置,将访问 Slave 机器的请求透传到 Master 机器,再进行 MySQL 操作。
通过增加 Proxy 进行请求透传,解决了 MySQL Client 分裂导致有可能读取到旧数据的问题,如图9所示。
图9 实现一个可靠日志存储保证各 MySQL 的数据一致
多机自动选主最常见的实现方式是由各个参与者发起投票,获得多数派支持的机器为 Master,同时把 Master 信息记录到可靠存储。 Master 机器定期到可靠存储延长租约;非 Master 机器定期检查 Master 租约是否过期,从而决定是否要发起选举自己为 Master 的投票。为了避免修改 MySQL 代码,在 MySQL 机器上增加一个Agent,由 Agent 来替代 MySQL 发起选主投票和续期租约;可靠存储继续由 BinlogSvr 承担。
Agent 完成以下功能:1. Master 机器的 Agent 监控本机 MySQL 是否正常服务;如果正常服务,则定期到可靠存储延长租约,否则停止续约。2. 非 Master 机器的 Agent 定期从可靠存储检查 Master 租约是否过期;如果过期,再检查本机 MySQL 是否已经执行了所有 Binlog。如果已经执行了所有 Binlog,则发起选举自己为 Master 的投票,如图10所示。
图10 可靠日志存储和 Agent 共同实现自动选主机制
从上述思路可以得出 PhxSQL 的简单三层架构。对于每一个节点,部署3个模块(PhxSQL Proxy,MySQL,PhxBinlogSvr)。多个节点上的 PhxBinlogSvr 组成一个可靠的日志存储集群和可靠的 Master 信息存储集群;PhxBinlogSvr 同时承担 Agent 的责任。 PhxSQL Proxy 负责请求的透传。Master 结点上的 PhxSync 负责将 MySQL 的 Binlog 发送到 PhxBinlogSvr,如图11所示。
图11 PhxSQL 基本架构
请求透传是 Proxy 主要的功能。主要解决在进行 Master 切换的时候, MySQL Client 会被分裂,不同的 Client 可能连接到不同的 MySQL。导致出现 MySQL Client 写入数据到错误的 Master 或者从错误的 Master 读取到错误的数据。
Proxy 的请求透传分两种:1. 读写端口请求透传:Slave 节点收到的请求透传给 Master 节点执行。 Master 节点收到的请求直接透传给本机 MySQL 执行。2. 只读端口请求透传:Master 节点收到的请求透传给 Slave 节点执行。Slave 节点收到的请求直接透传给本机 MySQL 执行,如图12所示。
图12 Proxy 请求透传流程
由于 Proxy 接管了 MySQL Client 的请求,为了使整个集群的读写性能接近单机 MySQL,Proxy 使用协程模型提高自身的处理能力。
Proxy 的协程模型使用开源的 Libco 库。Libco 库是微信团队开源的一个高性能协程库,具有以下特点:1. 用同步方式写代码,实现异步代码的性能。2. 支持千万级的并发连接。3. 项目地址https://github.com/tencent-wechat/libco
为了已有的应用程序能够不做任何修改就能迁移到 PhxSQL,Proxy 需兼容 MySQL 的所有功能。
MySQL 事务管理基于连接,同一个事务的所有请求通过同一个连接通信。在事务处理中连接丢失,事务将被 rollback(http://dev. MySQL .com/doc/refman/5.6/en/innodb-autocommit-commit-rollback.html)。
Proxy 使用1:1连接模型完全兼容 MySQL 事务。每当 MySQL Client发起一个连接到 Proxy,Proxy 都会相应地发起一个连接到 MySQL。两条连接中,任意一个中断,另外一个也相应断开,对应的事务会被 rollback,如图13所示。
图13 Proxy 的1对1事务连接模型
MySQL 的权限管理基于(用户,源 IP)对,源 IP 是通过 socket 句柄反查获取。当请求通过 Proxy 连接到 MySQL 时,源 IP 为 Proxy 本地 IP,权限管理会出现异常。
Proxy 利用 MySQL 协议 HEAD 保留字段透传真实源 IP 到 MySQ,MySQL 再从 HEAD 保留字段获取正确的源 IP 进行权限管理,如图14所示。
图14 Proxy 通过修改 MySQL 协议兼容 MySQL 权限
PhxSync 的功能和 MySQL 的 semisync 插件类似。经过调研,对 semisync 插件的接口做少量的调整,就可以使用这些插件接口来实现 PhxSync。
PhxSync 功能主要是:
1) 正常运行时提交 Binlog:
MySQL 在正常写入或者更新数据时,会调用 after _ flush 接口。PhxSync 插件通过实现 after _ flush 接口将 MySQL 新写入的 Binlog 提交到本机的 BinlogSvr,由本机 BinlogSvr 通过 Paxos 协议同步到 BinlogSvr 集群。
2) 重启时校准本地 Binlog:
MySQL 在重启时通过查询 BinlogSvr 集群判断本地Pending Binlog 的状态。如果 Pending Binlog 未复制到 BinlogSvr 集群则从本地删除,保持本地的 Binlog 数据和 BinlogSvr 集群的 Binlog 数据一致。
由于 MySQL 没有提供在重启时的插件接口,为了后续维护方便,在 MySQL 代码层抽象出了一个新插件接口 before _ binlog _ init 用于校准 Binlog。
上述对 after _ flush 接口的调整,和新增的 before _ binlog _ init 接口已经提交补丁给 MySQL 官方(http://bugs. MySQL .com/bug.php?id=83158)。
PhxBinlogSvr 主要负责存储 Binlog 和 Master 信息的维护。在数据复制阶段,通过 Paxos 协议保证 PhxBinlogSvr 各节点的数据一致性(下文称 PhxBinlogSvr 为 BinlogSvr)。
1) PhxPaxos 库
BinlogSvr 使用 PhxPaxos 库进行数据的复制。PhxPaxos 库是微信团队开源的 Paxos 类库,具有以下特性:1. 保证各节点的数据一致。2. 保证集群机器超过一半存活还能服务。3. 高性能。4. 功能完善。5. 稳定性经过大规模验证。6. 接口方便易用。7. 项目地址 https://github.com/tencent-wechat/phxpaxos
2) BinlogSvr 异常情况处理
防止 Slave 的节点提交数据
当旧 Master 在提交数据时由于网络问题数据包被卡在网络,且新 Mater 已经成功切换时,或者人为错误直接往 Slave 节点的 MySQL 写入数据时,则会出现 Slave 节点提交数据的情况。多节点同时提交数据会出现 BinlogSvr 的 Binlog 数据和 MySQL 存储的 Binlog 数据不一致的情况。
BinlogSvr 存储了集群内的 Master 信息。当其收到 MySQL 提交的数据时,可根据 Master 信息拒绝非 Master 节点的提交,如图15所示。
图15 BinlogSvr 通过 Master 信息拒绝非 Master 节点的提交
防止 Master 提交错误数据
在某些情况下,Master 可能会重新发送数据或者发送错误数据。譬如在网络不好的情况下 Master 由于提交数据超时而重发数据。磁盘发生故障或者数据被错误回滚或者修改的时候,Master 会提交错误的数据。
BinlogSvr 使用乐观锁机制来防止 Master 的异常提交。在 MySQL 提交数据给 BinlogSvr 时,以本机 MySQL 已经执行的 GTID 为乐观锁,提交的内容为(本机 MySQL 已经执行的最新 GTID,本次要提交的 Binlog)。BinlogSvr 通过检查请求中(本机 MySQL 已经执行的最新 GTID)和自身保存的最新 GTID 是否匹配来拒绝重新发送或者异常发送的数据,如图16所示。
图16 BinlogSvr 使用乐观锁拒绝 Master 在数据异常的情况下提交数据
为了让 Slave 能从 BinlogSvr 获取 Binlog,最好的方式就是 BinlogSvr 支持 MySQL 原生的复制协议,这样不用对 Slave 做任何修改,如图17所示。
图17 BinlogSvr 支持 MySQL 使用原生复制协议获取 Binlog 数据
BinlogSvr 除了存储 MySQL 的 Binlog 数据,还存储了 Master 信息。同时还承担了 Agent 的角色,负责监控 MySQL 的状态,必要时发起选举自己为 Master 的投票。
BinlogSvr 通过 Paxos 协议进行 Master 选举,选举成功后成为 Master 并拥有租约。通过 Paxos 协议选举保证了最终只产生一个 Master 且每个节点记录了一致的 Master 信息。
通过比较 PhxSQL 集群中各节点的数据(MySQL Binlog,PhxPaxos,BinlogSvr) 判断各节点数据是否一致,如图18所示。
图18 PhxSQL 3机数据对比
通过观察 Master 宕机时各节点的流量变化判断 Master 是否顺利切换。下图中的红线代表流量。当 Master 宕机时,流量会随之转移,代表 Master 顺利切换,如图19所示。
图19 PhxSQL 进行 Master 切换时各节点的写入流量变化
MySQL 版本:Percona 5.6.31-77.0机器信息:1. CPU :Intel Xeon CPU E5-2420 0 @ 1.90GHz * 24。2. Memory : 32G。3. Disk:SSD Raid10。 4. Ping Costs:Master→Slave:3 ~ 4ms; client→Master :4ms。
工具和参数:
PhxSQL 的写性能比 MySQL 的半同步好,读性能由于多了一层 Proxy 导致比 MySQL 的半同步稍差。
图20 PhxSQL 和 MySQL 的性能对比
QQ 邮箱(域名邮箱)域名记录服务器:单个集群调用峰值 40w/min。写请求平均耗时在 20ms 以下。读写比为20:1。机器配置:Intel Xeon CPU x3440 @ 2.53ghz 8 core ,8GB ram。
阅读全文: http://gitbook.cn/gitchat/geekbook/5a791a20aa4fa8335f4ea3a0