在技术方面,我自己热衷于 Open Source,写了很多 Open Source 的东西,擅长的是 Infrastructure 领域。Infrastructure 领域现在范围很广,比如说很典型的分布式 Scheduler、Mesos、Kubernetes,另外它和 Microservices 所结合的东西也特别多。Infrastructure 领域还有比如 Database 有分 AP(分析型)和 TP(事务型),比如说很典型的大家知道的 Spark、Greenplum、Apache Phoenix 等等,这些都属于在 AP 的,它们也会去尝试支持有限的 TP。另外,还有一个比较有意思的就是 Kudu——Cloudera Open Source 的那个项目,它的目标很有意思:我不做最强的 AP 系统,也不做最强的 TP 系统,我选择一个相对折中的方案。从文化哲学上看,它比较符合中国的中庸思想。

另外,我先后创建了 Codis、TiDB。去年12月份创建了 TiKV 这个 project,TiKV 在所有的 rust 项目里目前排名前三。

首先我们聊聊 Database 的历史,在已经有这么多种数据库的背景下我们为什么要创建另外一个数据库;以及说一下现在方案遇到的困境,说一下 Google Spanner 和 F1、TiKV 和 TiDB,说一下架构的事情,在这里我们会重点聊一下 TiKV。因为我们产品的很多特性是 TiKV 提供的,比如说跨数据中心的复制、Transaction、auto-scale。

接下来聊一下为什么 TiKV 用 Raft 能实现所有这些重要的特性,以及 scale、MVCC 和事务模型。东西非常多,我今天不太可能把里面的技术细节都描述得特别细,因为几乎每一个话题都可以找到一篇或者是多篇论文,所以详细的技术问题大家可以单独来找我聊。

后面再说一下我们现在遇到的窘境,就是大家常规遇到的分布式方案有哪些问题,比如 MySQL Sharding。我们创建了无数 MySQL Proxy,比如官方的 MySQL proxy、Youtube 的 Vitess、淘宝的 Cobar、TDDL以及基于 Cobar 的 MyCAT、金山的 Kingshard、360 的 Atlas、京东的 JProxy,我在豌豆荚也写了一个。可以说,随便一个大公司都会造一个 MySQL Sharding 的方案。

为什么我们要创建另外一个数据库?

昨天晚上我还跟一个同学聊到,基于 MySQL 的方案它的天花板在哪里,它的天花板特别明显。有一个思路是能不能通过 MySQL 的 server 把 InnoDB 变成一个分布式数据库,听起来这个方案很完美,但是很快就会遇到天花板。因为 MySQL 生成的执行计划是个单机的,它认为整个计划的 cost 也是单机的,我读取一行和读取下一行之间的开销是很小的,比如迭代 next row 可以立刻拿到下一行。实际上在一个分布式系统里面,这是不一定的。

另外,你把数据都拿回来计算这个太慢了,很多时候我们需要把我们的 expression 或者计算过程等等运算推下去,向上返回一个最终的计算结果,这个一定要用分布式的 plan,前面控制执行计划的节点,它必须要理解下面是分布式的东西,才能生成最好的 plan,这样才能实现最高的执行效率。

比如说你做一个 sum,你是一条条拿回来加,还是让一堆机器一起算,最后给我一个结果。 例如我有 100 亿条数据分布在 10 台机器上,并行在这 10台机器我可能只拿到 10 个结果,如果把所有的数据每一条都拿回来,这就太慢了,完全丧失了分布式的价值。聊到 MySQL 想实现分布式,另外一个实现分布式的方案就是 Proxy。但是 Proxy 本身的天花板在那里,就是它不支持分布式的 transaction,它不支持跨节点的 join,它无法理解复杂的 plan,一个复杂的 plan 打到 Proxy 上面,Proxy 就傻了,我到底应该往哪一个节点上转发呢,如果我涉及到 subquery sql 怎么办?所以这个天花板是瞬间会到,在传统模型下面的修改,很快会达不到我们的要求。

另外一个很重要的是,MySQL 支持的复制方式是半同步或者是异步,但是半同步可以降级成异步,也就是说任何时候数据出了问题你不敢切换,因为有可能是异步复制,有一部分数据还没有同步过来,这时候切换数据就不一致了。前一阵子出现过某公司突然不能支付了这种事件,今年有很多这种类似的 case,所以微博上大家都在说“说好的异地多活呢?”……

为什么传统的方案在这上面解决起来特别的困难,天花板马上到了,基本上不可能解决这个问题。另外是多数据中心的复制和数据中心的容灾,MySQL 在这上面是做不好的。

在前面三十年基本上是关系数据库的时代,那个时代创建了很多伟大的公司,比如说 IBM、Oracle、微软也有自己的数据库,早期还有一个公司叫 Sybase,有一部分特别老的程序员同学在当年的教程里面还可以找到这些东西,但是现在基本上看不到了。

另外是 NoSQL。NoSQL 也是一度非常火,像 Cassandra、MongoDB 等等,这些都属于在互联网快速发展的时候创建这些能够 scale 的方案,但 Redis scale 出来比较晚,所以很多时候大家把 Redis 当成一个 Cache,现在慢慢大家把它当成存储不那么重要的数据的数据库。因为它有了 scale 支持以后,大家会把更多的数据放在里面。

然后到了 2015,严格来讲是到 2014 年到 2015 年之间,Raft 论文发表以后,真正的 NewSQL 的理论基础终于完成了。我觉得 NewSQL 这个理论基础,最重要的划时代的几篇论文,一个是谷歌的 Spanner,是在 2013 年初发布的;再就是 Raft 是在 2014 年上半年发布的。这几篇相当于打下了分布式数据库 NewSQL 的理论基础,这个模型是非常重要的,如果没有模型在上面是堆不起来东西的。说到现在,大家可能对于模型还是可以理解的,但是对于它的实现难度很难想象。

前面我大概提到了我们为什么需要另外一个数据库,说到 Scalability 数据的伸缩,然后我们讲到需要 SQL,比如你给我一个纯粹的 key-velue 系统的 API,比如我要查找年龄在 10 岁到 20 岁之间的 email 要满足一个什么要求的。如果只有 KV 的 API 这是会写死人的,要写很多代码,但是实际上用 SQL 写一句话就可以了,而且 SQL 的优化器对整个数据的分布是知道的,它可以很快理解你这个 SQL,然后会得到一个最优的 plan,他得到这个最优的 plan 基本上等价于一个真正理解 KV 每一步操作的人写出来的程序。通常情况下,SQL 的优化器是为了更加了解或者做出更好的选择。

另外一个就是 ACID 的事务,这是传统数据库必须要提供的基础。以前你不提供 ACID 就不能叫数据库,但是近些年大家写一个内存的 map 也可以叫自己是数据库。大家写一个 append-only 文件,我们也可以叫只读数据库,数据库的概念比以前极大的泛化了。

另外就是高可用和自动恢复,他们的概念是什么呢?有些人会有一些误解,因为今天还有朋友在现场问到,出了故障,比如说一个机房挂掉以后我应该怎么做切换,怎么操作。这个实际上相当于还是上一代的概念,还需要人去干预,这种不算是高可用。

未来的高可用一定是系统出了问题马上可以自动恢复,马上可以变成可用。比如说一个机房挂掉了,十秒钟不能支付,十秒钟之后系统自动恢复了变得可以支付,即使这个数据中心再也不起来我整个系统仍然是可以支付的。Auto-Failover 的重要性就在这里。大家不希望在睡觉的时候被一个报警给拉起来,我相信大家以后具备这样一个能力,5 分钟以内的报警不用理会,挂掉一个机房,又挂掉一个机房,这种连续报警才会理。我们内部开玩笑说,希望大家都能睡个好觉,很重要的事情就是这个。

说完应用层的事情,现在很有很多业务,在应用层自己去分片,比如说我按照 user ID在代码里面分片,还有一部分是更高级一点我会用到一致性哈希。问题在于它的复杂度,到一定程度之后我自动的分库,自动的分表,我觉得下一代数据库是不需要理解这些东西的,不需要了解什么叫做分库,不需要了解什么叫做分表,因为系统是全部自动搞定的。同时复杂度,如果一个应用不支持事务,那么在应用层去做,通常的做法是引入一个外部队列,引入大量的程序机制和状态转换,A 状态的时候允许转换到 B 状态,B 状态允许转换到 C 状态。

举一个简单的例子,比如说在京东上买东西,先下订单,支付状态之后这个商品才能出库,如果不是支付状态一定不能出库,每一步都有严格的流程。

Google Spanner / F1

说一下 Google 的 Spanner 和 F1,这是我非常喜欢的论文,也是我最近几年看过很多遍的论文。 Google Spanner 已经强大到什么程度呢?Google Spanner 是全球分布的数据库,在国内目前普遍做法叫做同城两地三中心,它们的差别是什么呢?以 Google 的数据来讲,谷歌比较高的级别是他们有 7 个副本,通常是美国保存 3 个副本,再在另外 2 个国家可以保存 2 个副本,这样的好处是万一美国两个数据中心出了问题,那整个系统还能继续可用,这个概念就是比如美国 3 个副本全挂了,整个数据都还在,这个数据安全级别比很多国家的安全级别还要高,这是 Google 目前做到的,这是全球分布的好处。

现在国内主流的做法是两地三中心,但现在基本上都不能自动切换。大家可以看到很多号称实现了两地三中心或者异地多活,但是一出现问题都说不好意思这段时间我不能提供服务了。大家无数次的见到这种 case, 我就不列举了。

Spanner 现在也提供一部分 SQL 特性。在以前,大部分 SQL 特性是在 F1 里面提供的,现在 Spanner 也在逐步丰富它的功能,Google 是全球第一个做到这个规模或者是做到这个级别的数据库。事务支持里面 Google 有点黑科技(其实也没有那么黑),就是它有GPS 时钟和原子钟。大家知道在分布式系统里面,比如说数千台机器,两个事务启动先后顺序,这个顺序怎么界定(事务外部一致性)。这个时候 Google 内部使用了 GPS 时钟和原子钟,正常情况下它会使用一个GPS 时钟的一个集群,就是说我拿的一个时间戳,并不是从一个 GPS 上来拿的时间戳,因为大家知道所有的硬件都会有误差。如果这时候我从一个上拿到的 GPS 本身有点问题,那么你拿到的这个时钟是不精确的。而 Google 它实际上是在一批 GPS 时钟上去拿了能够满足 majority 的精度,再用时间的算法,得到一个比较精确的时间。大家知道 GPS 也不太安全,因为它是美国军方的,对于 Google 来讲要实现比国家安全级别更高的数据库,而 GPS 是可能受到干扰的,因为 GPS 信号是可以调整的,这在军事用途上面很典型的,大家知道导弹的制导需要依赖 GPS,如果调整了 GPS 精度,那么导弹精度就废了。所以他们还用原子钟去校正 GPS,如果 GPS 突然跳跃了,原子钟上是可以检测到 GPS 跳跃的,这部分相对有一点黑科技,但是从原理上来讲还是比较简单,比较好理解的。

最开始它 Spanner 最大的用户就是 Google 的 Adwords,这是 Google 最赚钱的业务,Google 就是靠广告生存的,我们一直觉得 Google 是科技公司,但是他的钱是从广告那来的,所以一定程度来讲 Google 是一个广告公司。Google 内部的方向先有了 Big table ,然后有了 MegaStore ,MegaStore 的下一代是 Spanner ,F1 是在 Spanner 上面构建的。

TiDB and TiKV

TiKV 和 TiDB 基本上对应 Google Spanner 和 Google F1,用 Open Source 方式重建。目前这两个项目都开放在 GitHub 上面,两个项目都比较火爆,TiDB 是更早一点开源的, 目前 TiDB 在 GitHub 上 有 4300 多个 Star,每天都在增长。

另外,对于现在的社会来讲,我们觉得 Infrastructure 领域闭源的东西是没有任何生存机会的。没有任何一家公司,愿意把自己的身家性命压在一个闭源的项目上。举一个很典型的例子,在美国有一个数据库叫 FoundationDB,去年被苹果收购了。FoundationDB 之前和用户签的合约都是一年的合约。比如说,我给你服务周期是一年,现在我被另外一个公司收购了,我今年服务到期之后,我是满足合约的。但是其他公司再也不能找它服务了,因为它现在不叫 FoundationDB 了,它叫 Apple了,你不能找 Apple 给你提供一个 Enterprise service。

TiDB 和 TiKV 为什么是两个项目,因为它和 Google 的内部架构对比差不多是这样的:TiKV 对应的是 Spanner,TiDB 对应的是 F1 。F1 里面更强调上层的分布式的 SQL 层到底怎么做,分布式的 Plan 应该怎么做,分布式的 Plan 应该怎么去做优化。同时 TiDB 有一点做的比较好的是,它兼容了 MySQL 协议,当你出现了一个新型的数据库的时候,用户使用它是有成本的。大家都知道作为开发很讨厌的一个事情就是,我要每个语言都写一个 Driver,比如说你要支持 C++、你要支持 Java、你要支持 Go 等等,这个太累了,而且用户还得改他的程序,所以我们选择了一个更加好的东西兼容 MySQL 协议,让用户可以不用改。一会我会用一个视频来演示一下,为什么一行代码不改就可以用,用户就能体会到 TiDB 带来的所有的好处。

这个图实际上是整个协议栈或者是整个软件栈的实现。大家可以看到整个系统是高度分层的,从最底下开始是 RocksDB ,然后再上面用 Raft 构建一层可以被复制的 RocksDB ,在这一层的时候它还没有 Transaction,但是整个系统现在的状态是所有写入的数据一定要保证它复制到了足够多的副本。也就是说只要我写进来的数据一定有足够多的副本去 cover 它,这样才比较安全,在一个比较安全的 Key-value store 上面, 再去构建它的多版本,再去构建它的分布式事务,然后在分布式事务构建完成之后,就可以轻松的加上 SQL 层,再轻松的加上MySQL 协议的支持。然后,这两天我比较好奇,自己写了 MongoDB 协议的支持,然后我们可以用 MongoDB 的客户端来玩,就是说协议这一层是高度可插拔的。TiDB 上可以在上面构建一个 MongoDB 的协议,相当于这个是构建一个 SQL 的协议,可以构建一个 NoSQL 的协议。这一点主要是用来验证 TiKV 在模型上面的支持能力。

这是整个 TiKV 的架构图,从这个看来,整个集群里面有很多 Node,比如这里画了四个 Node ,分别对应了四个机器。每一个 Node 上可以有多个 Store,每个 Store 里面又会有很多小的 Region,就是说一小片数据,就是一个 Region 。从全局来看所有的数据被划分成很多小片,每个小片默认配置是 64M,它已经足够小,可以很轻松的从一个节点移到另外一个节点,Region 1 有三个副本,它分别在 Node1、Node 2 和 Node4 上面, 类似的Region 2,Region 3 也是有三个副本。每个 Region 的所有副本组成一个 Raft Group,整个系统可以看到很多这样的 Raft groups。

Raft 细节我不展开了,大家有兴趣可以找我私聊或者看一下相应的资料。

因为整个系统里面我们可以看到上一张图里面有很多 Raft group 给我们,不同 Raft group 之间的通讯都是有开销的。所以我们有一个类似于 MySQL 的 group commit 机制 ,你发消息的时候实际上可以 share 同一个 connection , 然后 pipeline + batch 发送,很大程度上可以省掉大量 syscall 的开销。

另外,其实在一定程度上后面我们在支持压缩的时候,也有非常大的帮助,就是可以减少数据的传输。对于整个系统而言,可能有数百万的 Region,它的大小可以调整,比如说 64M、128M、256M,这个实际上依赖于整个系统里面当前的状况。

比如说我们曾经在有一个用户的机房里面做过测试,这个测试有一个香港机房和新加坡的机房。结果我们在做复制的时候,新加坡的机房大于 256M 就复制不过去,因为机房很不稳定,必须要保证数据切的足够小,这样才能复制过去。

如果一个 Region 太大以后我们会自动做 SPLIT,这是非常好玩的过程,有点像细胞的分裂。

然后 TiKV 的 Raft 实现,是从 etcd 里面 port 过来的,为什么要从 etcd 里面 port 过来呢?首先 TiKV 的 Raft 实现是用 Rust 写的。作为第一个做到生产级别的 Raft 实现,所以我们从 etcd 里面把它用 Go 语言写的 port 到这边。

这个是 Raft 官网上面列出来的 TiKV在里面的状态,大家可以看到 TiKV 把所有 Raft 的 feature 都实现了。 比如说 Leader Election、Membership Changes,这个是非常重要的,整个系统的 scale 过程高度依赖 Membership Changes,后面我用一个图来讲这个过程。后面这个是 Log Compaction,这个用户不太关心。

这是很典型的细胞分裂的图,实际上 Region 的分裂过程和这个是类似的。

我们看一下扩容是怎么做的。

比如说以现在的系统假设,我们刚开始说只有三个节点,有 Region1 分别是在 1 、2、4,我用虚线连接起来代表它是一个 Raft group ,大家可以看到整个系统里面有三个 Raft group ,在每一个 Node 上面数据的分布是比较均匀的,在这个假设每一个 Region 是 64M ,相当于只有一个 Node 上面负载比其他的稍微大一点点。

一个在线视频默认我们都是推荐 3 个副本或者 5 个副本的配置。Raft 本身有一个特点,如果一个 leader down 掉之后,其它的节点会选一个新的 leader ,那么这个新的 leader 会把它还没有 commit 但已经 reply 过去的 log 做一个 commit ,然后会再做 apply ,这个有点偏 Raft 协议,细节我不讲了。

复制数据的小的 Region,它实际上是跨多个数据中心做的复制。这里面最重要的一点是永远不丢失数据,无论如何我保证我的复制一定是复制到 majority ,任何时候我只要对外提供服务,允许外面写入数据一定要复制到 majority 。很重要的一点就是恢复的过程一定要是自动化的,我前面已经强调过,如果不能自动化恢复,那么中间的宕机时间或者对外不可服务的时间,便不是由整个系统决定的,这是相对回到了几十年前的状态。

MVCC

MVCC 我稍微仔细讲一下这一块。MVCC 的好处,它很好支持 Lock-free 的 snapshot read ,一会儿我有一个图会展示 MVCC 是怎么做的。isolation level 就不讲了, MySQL 里面的级别是可以调的,我们的 TiKV 有 SI,还有 SI+lock,默认是支持 SI 的这种隔离级别,然后你写一个 select for update 语句,这个会自动的调整到 SI 加上 lock 这个隔离级别。这个隔离级别基本上和 SSI 是一致的。还有一个就是 GC 的问题,如果你的系统里面的数据产生了很多版本,你需要把这个比较老的数据给 GC 掉,比如说正常情况下我们是不删除数据的, 你写入一行,然后再写入一行,不断去 update 同一行的时候,每一次 update 会产生新的版本,新的版本就会在系统里存在,所以我们需要一个 GC 的模块把比较老的数据给 GC 掉,实际上这个 GC 不是 Go 里面的GC,不是 Java 的 GC,而是数据的 GC。

这是一个数据版本,大家可以看到我们的数据分成两块,一个是 meta,一个是 data。meta 相对于描述我的数据当前有多少个版本。大家可以看到绿色的部分,比如说我们的 meta key 是 A ,keyA 有三个版本,是 A1 、A2、A3,我们把 key 自己和 version 拼到一起。那我们用 A1、A2、A3 分别描述 A 的三个版本,那么就是 version 1/2/3。meta 里面描述,就是我的整个 key 相对应哪个版本,我想找到那个版本。比如说我现在要读取 key A 的版本10,但显然现在版本 10 是没有的,那么小于版本 10 最大的版本是 3,所以这时我就能读取到 3,这是它的隔离级别决定的。关于 data,我刚才已经讲过了。

分布式事务模型

接下来是分布式事务模型,其实是基于 Google Percolator,这是 Google 在 2006 发表的一篇论文,是 Google 在做内部增量处理的时候发现了这个方法,本质上还是二阶段提交的。这使用的是一个乐观锁,比如说我提供一个 transaction ,我去改一个东西,改的时候是发布在本地的,并没有马上 commit 到数据存储那一端,这个模型就是说,我修改的东西我马上去 Lock 住,这个基本就是一个悲观锁。但如果到最后一刻我才提交出去,那么锁住的这一小段的时间,这个时候实现的是乐观锁。乐观锁的好处就是当你冲突很小的时候可以得到非常好的性能,因为冲突特别小,所以我本地修改通常都是有效的,所以我不需要去 Lock ,不需要去 roll back 。本质上分布式事务就是 2PC (两阶段提交) 或者是 2+x PC,基本上没有 1PC,除非你在别人的级别上做弱化。比如说我允许你读到当前最新的版本,也允许你读到前面的版本,书里面把这个叫做幻读。如果你调到这个程度是比较容易做 1PC 的,这个实际上还是依赖用户设定的隔离级别的,如果用户需要更高的隔离级别,这个 1PC就不太好做了。

这是一个路由,正常来讲,大家可能会好奇一个 SQL 语句怎么最后会落到存储层,然后能很好的运行,最后怎么能映射到 KV 上面,又怎么能路由到正确的节点,因为整个系统可能有上千个节点,你怎么能正确路由到那一个的节点。我们在 TiDB 有一个 TiKV driver , 另外 TiKV 对外使用的是 Google Protocol Buffer 来作为通讯的编码格式。

Placement Driver

来说一下 Placement Driver 。Placement Driver 是什么呢?整个系统里面有一个节点,它会时刻知道现在整个系统的状态。比如说每个机器的负载,每个机器的容量,是否有新加的机器,新加机器的容量到底是怎么样的,是不是可以把一部分数据挪过去,是不是也是一样下线, 如果一个节点在十分钟之内无法被其他节点探测到,我认为它已经挂了,不管它实际上是不是真的挂了,但是我也认为它挂了。因为这个时候是有风险的,如果这个机器万一真的挂了,意味着你现在机器的副本数只有两个,有一部分数据的副本数只有两个。那么现在你必须马上要在系统里面重新选一台机器出来,它上面有足够的空间,让我现在只有两个副本的数据重新再做一份新的复制,系统始终维持在三个副本。整个系统里面如果机器挂掉了,副本数少了,这个时候应该会被自动发现,马上补充新的副本,这样会维持整个系统的副本数。这是很重要的 ,为了避免数据丢失,必须维持足够的副本数,因为副本数每少一个,你的风险就会再增加。这就是 Placement Driver 做的事情。

同时,Placement Driver 还会根据性能负载,不断去 move 这个 data 。比如说你这边负载已经很高了,一个磁盘假设有 100G,现在已经用了 80G,另外一个机器上也是 100G,但是他只用了 20G,所以这上面还可以有几十 G 的数据,比如 40G 的数据,你可以 move 过去,这样可以保证系统有很好的负载,不会出现一个磁盘巨忙无比,数据已经多的装不下了,另外一个上面还没有东西,这是 Placement Driver 要做的东西。

Raft 协议还提供一个很高级的特性叫 leader transfer。leader transfer 就是说在我不移动数据的时候,我把我的 leadership 给你,相当于从这个角度来讲,我把流量分给你,因为我是 leader,所以数据会到我这来,但我现在把 leader给你,我让你来当 leader,原来打给我的请求会被打给你,这样我的负载就降下来。这就可以很好的动态调整整个系统的负载,同时又不搬移数据。不搬移数据的好处就是,不会形成一个抖动。

MySQL Sharding

MySQL Sharding 我前面已经提到了它的各种天花板,MySQL Sharding 的方案很典型的就是解决基本问题以后,业务稍微复杂一点,你在 sharding 这一层根本搞不定。它永远需要一个 sharding key,你必须要告诉我的 proxy,我的数据要到哪里找,对用户来说是极不友好的,比如我现在是一个单机的,现在我要切入到一个分布式的环境,这时我必须要改我的代码,我必须要知道我这个 key ,我的 row 应该往哪里 Sharding。如果是用 ORM ,这个基本上就没法做这个事情了。有很多 ORM 它本身假设我后面只有一个 MySQL。但 TiDB 就可以很好的支持,因为我所有的角色都是对的,我不需要关注 Sharding 、分库、分表这类的事情。

这里面有一个很重要的问题没有提,我怎么做 DDL。如果这个表非常大的话,比如说我们有一百亿吧,横跨了四台机器,这个时候你要给它做一个新的 Index,就是我要添加一个新的索引,这个时候你必须要不影响任何现有的业务,实际上这是多阶段提交的算法,这个是 Google 和 F1 一起发出来那篇论文。

简单来讲是这样的,先把状态标记成 delete only ,delete only 是什么意思呢?因为在分布式系统里面,所有的系统对于 schema 的视野不是一致的,比如说我现在改了一个值,有一部分人发现这个值被改了,但是还有一部分人还没有开始访问这个,所以根本不知道它被改了。然后在一个分布系统里,你也不可能实时通知到所有人在同一时刻发现它改变了。比如说从有索引到没有索引,你不能一步切过去,因为有的人认为它有索引,所以他给它建了一个索引,但是另外一个机器他认为它没有索引,所以他就把数据给删了,索引就留在里面了。这样遇到一个问题,我通过索引找的时候告诉我有, 实际数据却没有了,这个时候一致性出了问题。比如说我 count 一个 email 等于多少的,我通过 email 建了一个索引,我认为它是在,但是 UID 再转过去的时候可能已经不存在了。

比如说我先标记成 delete only,我删除它的时候不管它现在有没有索引,我都会尝试删除索引,所以我的数据是干净的。如果我删除掉的话,我不管结果是什么样的,我尝试去删一下,可能这个索引还没 build 出来,但是我仍然删除,如果数据没有了,索引一定没有了,所以这可以很好的保持它的一致性。后面再类似于前面,先标记成 write only 这种方式,连续再迭代这个状态,就可以迭代到一个最终可以对外公开的状态。比如说当我迭代到一定程度的时候,我可以从后台 build index ,比如说我一百亿,正在操作的 index 会马上 build,但是还有很多没有 build index ,这个时候后台不断的跑 map-reduce 去 build index ,直到整个都 build 完成之后,再对外 public ,就是说我这个索引已经可用了,你可以直接拿索引来找,这个是非常经典的。在这个 Online,Asynchronous Schema Change in F1 paper之前,大家都不知道这事该怎么做。

Proxy Sharding 的方案不支持分布式事务,更不用说跨数据中心的一致性事务了。 TiKV 很好的支持 transaction,刚才提到的 Raft 除了增加副本之外,还有 leader transfer,这是一个传统的方案都无法提供的特性。以及它带来的好处,当我瞬间平衡整个系统负载的时候,对外是透明的, 做 leader transfer 的时候并不需要移动数据,只是个简单的 leader transfer 消息。

然后说一下如果大家想参与我们项目的话是怎样的过程,因为整个系统是完全开源的,如果大家想参与其中任何一部分都可以,比如说我想参与到分布式 KV,可以直接贡献到 TiKV。TiKV 需要写 Rust,如果大家对这块特别有激情可以体验写 Rust 的感觉 。

TiDB 是用 Go 写的,Go 在中国的群众基础是非常多的,目前也有很多人在贡献。整个 TiDB 和TiKV 是高度协作的项目,因为 TiDB 目前还用到了 etcd ,我们在和 CoreOS 在密切的合作,也特别感谢 CoreOS 帮我们做了很多的支持,我们也为 CoreOS 的 etcd 提了一些 patch。同时,TiKV 使用 RocksDB ,所以我们也为 RocksDB 提了一些 patch 和 test,我们也非常感谢 Facebook RocksDB team 对我们项目的支持。

另外一个是 PD,就是我们前面提的 Placement Driver,它负责监控整个系统。这部分的算法比较好玩,大家如果有兴趣的话,可以去自己控制整个集群的调度,它和 Kubernetes 或者是Mesos 的调度算法是不一样的,因为它调度的维度实际上比那个要更多。比如说磁盘的容量,你的 leader 的数量,你的网络当前的使用情况,你的 IO 的负载和 CPU 的负载都可以放进去。同时你还可以让它调度不要跨一个机房里面建多个副本。

posted @ 2016-08-23 15:12 小马歌 阅读(268) | 评论 (0)编辑 收藏
 
from:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0831/3395.html

编辑推荐:稀土掘金,这是一个针对技术开发者的一个应用,你可以在掘金上获取最新最优质的技术干货,不仅仅是Android知识、前端、后端以至于产品和设计都有涉猎,想成为全栈工程师的朋友不要错过!

原文:http://www.csdn.net/article/2015-08-31/2825579 

JSON——大家可能都知道它是几乎所有现代服务器都使用的轻量级数据交换格式。它体量轻,可读性强,通常比老式的、不友好的XML开发起来更加便捷友好。JSON是不依赖于开发语言的数据格式,但是在解析数据并将其转换到如Java对象时,会消耗我们的时间和存储资源。

几天前,Facebook宣布,其Android应用程序大幅提升了数据处理性能。这是由于几乎在全部应用程序中放弃了JSON数据格式,用FlatBuffers取而代之了。阅读这篇文章可以获得关于FlatBuffers的基础知识,学会如何从JSON转换到FlatBuffers。

虽然这东西是非常有前景的,但是乍一看其实现过程不是一下子就能明白的。而且Facebook也没有说得很详细。这就是为什么我要写这篇文章,在其中展示我们是如何使用Flatbuffers开展工作的。

FlatBuffers

总之,FlatBuffers是Google专门为游戏开发而创建的跨平台序列化库,就像Facebook所展示的那样,它在Android平台上遵循快速响应UI的16ms规则

但是,在把所有数据迁移到FlatBuffers之前,你要确定确实需要这样做。因为,这样做有时对性能的影响是潜移默化的,而且数据安全性要比计算速度上几十毫秒的差异更重要。

什么使得Flatbuffers如此奏效?


  • 由于是以二进制形式缓存,访问序列化数据时也无需数据解析过程。即使对于层次化数据也不需要解析。多亏不需要初始化解析器(初始化意味着要建立复杂的字段映射)和解析数据,这些都是需要花费时间的。

  • Flatbuffers数据不需要分配比自身使用缓冲区还要多的内存。我们不必像在JSON中那样为解析数据的整个层次分配额外对象。

要获得正宗的数据,就再读一读Facebook上关于FlatBuffers迁移问题的文章,还有Google自己的文档。


实现

本文将介绍在Android应用程序中使用Flatbuffers的最简单方法:


  • JSON数据在应用程序之外的某个地方被转换成FlatBuffer格式的文件(例如,将二进制数据以文件的形式提交,还可以从API直接返回FlatBuffer二进制文件)。

  • 在flatc (FlatBuffer编译器)的帮助下,手工生成数据模型(Java类)。

  • JSON文件存在一定的局限性(不能使用null字段,日期格式也被解析为字符串)。


将来,我们或许会提出更复杂的解决方案。

FlatBuffers编译器

首先,我们需要flatc,即flatbuffers编译器。该编译器可以从Google所属的源代码构建,源代码位于Flatbuffers资源库中。我们下载并克隆它。整个构建过程在FlatBuffers构建文档中都做了描述。如果你是Mac用户的话,需要这样来构建:


  1. 在\{extract directory}\build\XcodeFlatBuffers.xcodeproj路径下,打开已下载的源代码。

  2. 点击Play按钮或⌘ + R,运行flatc scheme(默认情况下应该是被选中的)。

  3. flatc可执行文件就会在项目的根目录下出现。


现在,我们可以使用schema编译器了,该编译器能够把给定的schema(在Java、C#、Python、GO和C++语言中的schema)生成为模型类,还可以把JSON转换成Flatbuffer的二进制文件。

Schema文件

接着,我们必须准备schema文件,该文件定义了要进行序列化和反序列化的数据结构。这个schema将用于flatc创建Java模型,把JSON转换成FlatBuffers的二进制文件。

这里是JSON文件的一部分。

完整版本在这里。这是略微修改后的版本,可以从Github API调用:https://api.github.com/users/google/repos 。

Flatbuffer schema是编写得很好的文档,所以就不深入探讨这个问题了。另外,本文中的schema不会很复杂。我们所要做的仅仅是创建3张表:ReposList, Repo和User,并定义root_type。这是schema的重要组成部分。

完整的schema文件在这里

FlatBuffers数据文件

真棒,我们现在要做的是把repos_json.json转换成FlatBuffers二进制文件,生成能够以Java风格表示数据的Java模型(此处操作所需的全部文件都在我们的代码库中):

$ ./flatc -j -b repos_schema.fbs repos_json.json

如果一切顺利,会产生下列文件:


  • repos_json.bin(要将重命名它为 repos_flat.bin)

  • Repos/Repo.java

  • Repos/ReposList.java

  • Repos/User.java


Android应用程序

现在来创建示例程序,在实践中来看看Flatbuffers格式是如何起作用的。这是截图:

blob.png

在UI部分,ProgressBar仅用于显示不恰当的数据处理对用户界面顺畅度的影响。

应用文件看起来是这个样子:app/build.gradle

当然,在本例中不是必须要用Rx或ButterKnife这样的视图注入利器,但是为什么不让应用更细致一些呢??

我们把repos_flat.bin 和 repos_json.json文件放到res/raw/目录下。RawDataReader是工具类,它帮助我们读取Android应用中的原始文件。

最后,把Repo,ReposList和User这三张表对应的模型类代码放到项目源代码中。

FlatBuffers库

使用Java语言编程过程中,FlatBuffers提供了可以直接处理这种数据格式的库,也这是flatbuffers-java-1.2.0-SNAPSHOT.jar文件。如果你想手工生成该文件,需要下载FlatBuffers源代码,再到目录java/下,用Maven生成该库:

$ mvn install

现在将.jar文件放到Android项目的app/libs/目录下。

好了,当务之急是实现MainActivity类,这是完整源代码。

我们最为关注的两个方法是:


  • parseReposListJson(String reposStr) - 这个方法初始化Gson解析器,并把JSON字符串转换成Java对象。

  • loadFlatBuffer(byte[] bytes)  - 这个方法将字节(repos_flat.bin文件)转换成Java对象。


使用FlatBuffers的结果

现在让我们把JSON和FlatBuffers在加载时间和资源消耗方面的差异形象化。测试是在带有Android M(beta版)的Nexus 5上进行的。

加载时间

测量的过程是将其他文件转换为Java源文件,对所有(90个)元素进行迭代。


  • 使用JSON:JSON文件(大小:478kB)平均加载时间200ms(时间区间:180ms~250ms);

  • 使用FlatBuffers:FlatBuffers二进制文件(大小:352kB)平均加载时间5ms(时间区间:3ms~10ms)。


记得16ms规则吗?我们在UI线程中调用这些方法的原因就是要看看在这种情况下界面表现如何:

JSON数据加载效果:

json.gif

FlatBuffers数据加载效果:

flatbuffers.gif

看出区别了吗?JSON数据的加载过程中, ProgressBar停顿了一会,界面不是那么顺畅(加载时间超过了16ms)。

内存分配、CPU等资源

还有什么想要测量的吗?也许应该测量一下Android Studio 1.3,还有那些新特性。例如,内存分配跟踪器(Allocation Tracker),内存状态查看器(Memory Viewer)和方法跟踪器(Method Tracer)。

源代码

这里所讲解项目的完整源代码都在Github代码库中。你不需要接触整个FlatBuffers项目,所需的内容全都在flatbuffers/目录下。

( 翻译/张挥戈  友情审校/白云鹏)

文章来源:froger_mcs dev blog

作者简介:

Miroslaw Stanek,Azimo Money Transfer公司移动项目负责人,Android和iOS平台程序员,视频游戏玩家,冰雪运动爱好者。个人博客:http://frogermcs.github.io

posted @ 2016-08-18 16:26 小马歌 阅读(573) | 评论 (0)编辑 收藏
 
from:http://www.oschina.net/news/75092/android-flatbuffers-json

你可能会问,既然我们已经有很标准的JSON以及转换库比如GSON和Jackson,为什么还要使用新的工具呢?

不妨先试一下FlatBuffers,然后你就会发现它比JSON快得多。

FlatBuffers是什么?

FlatBuffers是一个高效的跨平台序列化类库,可以在C++、C#、C、Go、Java、JavaScript、PHP和Python中使用。是Google开发的,是为了应用在游戏开发,以及其他注重性能的应用上。

为什么要使用FlatBuffers?

  • 不需要解析/拆包就可以访问序列化数据 — FlatBuffers与其他库不同之处就在于它使用二进制缓冲文件来表示层次数据,这样它们就可以被直接访问而不需解析与拆包,同时还支持数据结构进化(前进、后退兼容性)。

  • 内存高效速度快 — 访问数据时只需要访问内存中的缓冲区。它不需要多余的内存分配(至少在C++是这样,其他语言中可能会有变动)。FlatBuffers还适合配合 mmap或数据流使用,只需要缓冲区的一部分存储在内存中。访问时速度接近原结构访问,只有一点延迟(一种虚函数表vtable),是为了允许格式升级以 及可选字段。FlatBuffers适合那些花费了大量时间和空间(内存分配)来访问和构建序列化数据的项目,比如游戏以及其他对表现敏感的应用。可以参 考这里的基准

  • 灵活 — 由于有可选字段,你不但有很强的升级和回退兼容性(对于历史悠久的游戏尤其重要,不用为了每个版本升级所有数据),在选择要存储哪些数据以及设计数据结构时也很自由。

  • 轻量的code footprint — FlatBuffers只需要很少量的生成代码,以及一个表示最小依赖的很小的头文件,很容易集成。细节上可以看上面的基准页。

  • 强类型 — 编译时报错,而不需要自己写重复的容易出错的运行时检查。它可以自动生成有用的代码。

  • 使用方便 — 生成的C++代码允许精简访问与构建代码。还有可选的用于实现图表解析、类似JSON的运行时字符串展示等功能的方法。(后者比JSON解析库更快,内存效率更高)

  • 代码跨平台且没有依赖 — C++代码可以运行在任何近代的gcc/clang和VS2010上。同时还有用于测试和范例的构建文件(Android中.mk文件,其他平台是cmake文件)。

都有谁使用FlatBuffers?

  • BobbleApp,印度第一贴图App。我们在BobbleApp中使用FlatBuffers后App的性能明显增强。

  • Cocos2d-x,第一开源移动游戏引擎,使用FlatBuffers来序列化所有的游戏数据。

  • Facebook使用FlatBuffers在Android App中进行客户端服务端的沟通。他们写了一篇文章来描述FlatBuffers是如何加速加载内容的。

  • Google的Fun Propulsion Labs在他们所有的库和游戏中大量使用FlatBuffers。

App性能有多大提高?

  • 解析速度 解析一个20KB的JSON流(这差不多是BobbleApp的返回大小)需要35ms,超过了UI刷新间隔也就是16.6ms。如果解析JSON的话,我们就在滑动时就会因为要从磁盘加载缓存而导致掉帧(视觉上的卡顿)。

  • 解析器初始化 一个JSON解析器需要先构建字段映射再进行解析,这会花100ms到200ms,很明显的拖缓App启动时间。

  • 垃圾回收 在解析JSON时创建了很多小对象,在我们的试验中,解析20kb的JSON流时,要分配大约100kb的瞬时存储,对Java内存回收造成很大压力。

FlatBuffers vs JSON

我尝试使用FlatBuffers和JSON解析4mb的JSON文件。

FlatBuffers花了1-5ms,JSON花了大约2000ms。在使用FlatBuffers期间Android App中没有GC,而在使用JSON时发生了很多次GC。在使用JSON时UI完全卡住,所以真实使用时只能在后台线程进行解析。

如何使用FlatBuffer呢?

我在我的GitHub中写了一个示例,里面手把手教你如何使用FlatBuffer。

原文链接:https://medium.freecodecamp.com/why-consider-flatbuffer-over-json-2e4aa8d4ed07

稿源:infoQ  作者Amit Shekhar  译者程大治

posted @ 2016-08-18 16:17 小马歌 阅读(219) | 评论 (0)编辑 收藏
 

from:http://www.jdon.com/46111


本文来自Pivotal,分析了中国铁路总公司12306这个世界上最大的铁路系统的火车票预订系统。

在这个星球上人类最大的年度运动大概算是中国农历新年,又称春节。有3488万人次通过航空和235万人次通过铁路踏上他们的旅途。从历史上看,铁路旅行意味着排长龙队伍买票,中国铁路总公司(CRC)现在开始在网上卖火车票,提供比车站售票处或通过电话购买更方便的方法。

随着越来越多人的使用车票预订系统,12306铁路订票系统打破了其传统的RDBMS关系数据库系统,需要重新开始了一个新的项目,以改善原有系统性能和可伸缩性的问题,能够承受像春节度假旅游期间的尖峰的压力。目前该网站成为中国最受欢迎网站的之一。在这样严苛的访问条件下,系统出现以下很差用户体验:使用中断,性能差,预订错误,支付失败,票务确认出现问题等等。

中国铁道科学研究院的副主任朱剑圣首先解决性能问题,早在2011年,朱博士确保新系统解决基于下面两个性能瓶颈:

1.关系型数据库超负载,以至于不能处理传入的请求,无论是规模扩展性还是可靠性,都不能满足SLA要求的水平。 

2.UNIX服务器的计算能力不足以解决容量需求。

朱博士说:“传统关系型数据库和大型机的计算模式并不具有扩展性,系统不能基于内存扩展跨多个节点上运行。我们的网站证明了这一点,而试图扩展我们的遗留系统将变得非常昂贵”。

使用In-memory内存数据网格解决扩展性和可靠性
朱博士的团队开始寻找新的解决方案,大型机被发现和关系数据库有同样的瓶颈,在内存数据网格(IMDG)领域,他们发现了Pivotal GemFire,在海运货物系统 金融服务,航空,电子商务等多个行业都拥有成功解决最具挑战性的数据问题的良好记录。为了执行评估,朱博士和他的团队选择了国际综合系统公司(IISI)。IISI拥有强大为政府机构工作的跟踪记录,包括在开发交通运输解决方案,迁移遗留系统到云计算架构等方面经历,有与Pivotal GemFire合作经验。他们开始试点,相信了GemFire​​将满足性能,可扩展性和可用性的要求,包括能够在低成本硬件上运行。

IISI创造了一个概念证明和展示了GemFire几个优点​​。售票计算速度提高50到100倍。当负载增加时,响应时间保持10-100毫秒的延迟。他们可以看到,通过增加容量,能实现近乎线性增长的可扩展性和高可用性的能力。项目组在短短两个月内建立了一个试点,四个月后,新的在线系统全面部署,跨越5700火车站。

该小组负责铁路网上预订系统每年增长高达50%。他们的网站每天的平均水平预订250万票。

72台UNIX系统和关系数据库换成了10台初始和10个备份的x86服务器,这是一个更具成本效益的模式,能在内存中处理2TB或一个月的的火车票数据。

朱博士认为:“首先,Pivotal GemFire​​提供了一个在真实的测试环境的证明。然后,在生产环境面对意想不到的尖峰也是成功的,具体采取了一个迭代的方法来部署,克服了一系列大规模的挑战。在最近的2013春运期间,该系统具备了运行稳定的性能和正常运行时间。现在,我们有一个可靠,经济合理的生产体系​​,支持记录容量增长的空间。这个规模实现10-100毫秒的延迟。”

基于高可用性,冗余和故障切换机制上的GemFire​​提供了连续正常运行,它已超出了所有在该领域的CRC校验的指标,并帮助他们维护他们的SLA。

具体PDF下载

posted @ 2016-08-17 16:40 小马歌 阅读(290) | 评论 (0)编辑 收藏
 
     摘要: from:http://www.infoq.com/cn/news/2016/08/Elasticsearch-5-0-Elastic大家好,非常高兴能在这里给大家分享,首先简单自我介绍一下,我叫曾勇,是Elastic的工程师。Elastic将在今年秋季的时候发布一个Elasticsearch V5.0的大版本,这次的微信分享将给大家介绍一下5.0版里面的一些新的特性和改进。5.0? 天啦噜,你是...  阅读全文
posted @ 2016-08-13 14:56 小马歌 阅读(769) | 评论 (0)编辑 收藏
 
from:http://www.infoq.com/cn/articles/anatomy-of-an-elasticsearch-cluster-part02

剖析Elasticsearch集群系列涵盖了当今最流行的分布式搜索引擎Elasticsearch的底层架构和原型实例。

本文是这个系列的第二篇,我们将讨论Elasticsearch如何处理分布式的三个C((共识(consensus)、并发(concurrency)和一致(consistency))的问题、Elasticsearch分片的内部概念,比如translog(预写日志,WAL(Write Ahead Log)),以及Lucene中的段。

本系列已经得到原文著者Ronak Nathani的授权

在本系列的前一篇中,我们讨论了Elasticsearch的底层存储模型及CRUD(创建、读取、更新和删除)操作的工作原理。在本文中,我将分享Elasticsearch是如何应对分布式系统中的一些基本挑战的,以及分片的内部概念。这其中包括了一些操作方面的事情,Insight Data的工程师们已经在使用Elasticsearch构建的数据平台之上成功地实践并真正理解。我将在本文中主要讲述:

共识——裂脑问题及法定票数的重要性

共识是分布式系统的一项基本挑战。它要求系统中的所有进程/节点必须对给定数据的值/状态达成共识。已经有很多共识算法诸如RaftPaxos等,从数学上的证明了是行得通的。但是,Elasticsearch却实现了自己的共识系统(zen discovery),Elasticsearch之父Shay Banon在这篇文章中解释了其中的原因。zen discovery模块包含两个部分:

  • Ping: 执行节点使用ping来发现彼此
  • 单播(Unicast):该模块包含一个主机名列表,用以控制哪些节点需要ping通

Elasticsearch是端对端的系统,其中的所有节点彼此相连,有一个master节点保持活跃,它会更新和控制集群内的状态和操作。建立一个新的Elasticsearch集群要经过一次选举,选举是ping过程的一部分,在所有符合条件的节点中选取一个master,其他节点将加入这个master节点。ping间隔参数ping_interval的默认值是1秒,ping超时参数ping_timeout的默认值是3秒。因为节点要加入,它们会发送一个请求给master节点,加入超时参数join_timeout的默认值是ping_timeout值的20倍。如果master出现问题,那么群集中的其他节点开始重新ping以启动另一次选举。这个ping的过程还可以帮助一个节点在忽然失去master时,通过其他节点发现master。

注意:默认情况下,client节点和data节点不参与这个选举过程。可以在elasticsearch.yml配置文件中,通过设置discovery.zen.master_election.filter_client属性和discovery.zen.master_election.filter_data属性为false来改变这种默认行为。

故障检测的原理是这样的,master节点会ping所有其他节点,以检查它们是否还活着;然后所有节点ping回去,告诉master他们还活着。

如果使用默认的设置,Elasticsearch有可能遭到裂脑问题的困扰。在网络分区的情况下,一个节点可以认为master死了,然后选自己作为master,这就导致了一个集群内出现多个master。这可能会导致数据丢失,也可能无法正确合并数据。可以按照如下公式,根据有资格参加选举的节点数,设置法定票数属性的值,来避免爆裂的发生。

discovery.zen.minimum_master_nodes = int(# of master eligible nodes/2)+1

这个属性要求法定票数的节点加入新当选的master节点,来完成并获得新master节点接受的master身份。对于确保群集稳定性和在群集大小变化时动态地更新,这个属性是非常重要的。图a和b演示了在网络分区的情况下,设置或不设置minimum_master_nodes属性时,分别发生的现象。

注意:对于一个生产集群来说,建议使用3个节点专门做master,这3个节点将不服务于任何客户端请求,而且在任何给定时间内总是只有1个活跃。

我们已经搞清楚了Elasticsearch中共识的处理,现在让我们来看看它是如何处理并发的。

并发

Elasticsearch是一个分布式系统,支持并发请求。当创建/更新/删除请求到达主分片时,它也会被平行地发送到分片副本上。但是,这些请求到达的顺序可能是乱序的。在这种情况下,Elasticsearch使用乐观并发控制,来确保文档的较新版本不会被旧版本覆盖。

每个被索引的文档都拥有一个版本号,版本号在每次文档变更时递增并应用到文档中。这些版本号用来确保有序接受变更。为了确保在我们的应用中更新不会导致数据丢失,Elasticsearch的API允许我们指定文件的当前版本号,以使变更被接受。如果在请求中指定的版本号比分片上存在的版本号旧,请求失败,这意味着文档已经被另一个进程更新了。如何处理失败的请求,可以在应用层面来控制。Elasticsearch还提供了其他的锁选项,可以通过这篇来阅读。

当我们发送并发请求到Elasticsearch后,接下来面对的问题是——如何保证这些请求的读写一致?现在,还无法清楚回答,Elasticsearch应落在CAP三角形的哪条边上,我不打算在这篇文章里解决这个素来已久的争辩。

但是,我们要一起看下如何使用Elasticsearch实现写读一致。

一致——确保读写一致

对于写操作而言,Elasticsearch支持的一致性级别,与大多数其他的数据库不同,允许预检查,来查看有多少允许写入的可用分片。可选的值有quorumoneall。默认的设置为quorum,也就是说只有当大多数分片可用时才允许写操作。即使大多数分片可用,还是会因为某种原因发生写入副本失败,在这种情况下,副本被认为故障,分片将在一个不同的节点上重建。

对于读操作而言,新的文档只有在刷新时间间隔之后,才能被搜索到。为了确保搜索请求的返回结果包含文档的最新版本,可设置replication为sync(默认),这将使操作在主分片和副本碎片都完成后才返回写请求。在这种情况下,搜索请求从任何分片得到的返回结果都包含的是文档的最新版本。即使我们的应用为了更高的索引率而设置了replication=async,我们依然可以为搜索请求设置参数_preferenceprimary。这样,搜索请求将查询主分片,并确保结果中的文档是最新版本。

我们已经了解了Elasticsearch如何处理共识、并发和一致,让我们来看看分片内部的一些主要概念,正是这些特点让Elasticsearch成为一个分布式搜索引擎。

Translog(预写日志)

因为关系数据库的发展,预写日志(WAL)或者事务日志(translog)的概念早已遍及数据库领域。在发生故障的时候,translog能确保数据的完整性。translog的基本原理是,变更必须在数据实际的改变提交到磁盘上之前,被记录下来并提交。

当新的文档被索引或者旧的文档被更新时,Lucene索引将发生变更,这些变更将被提交到磁盘以持久化。这是一个很昂贵的操作,如果在每个请求之后都被执行。因此,这个操作在多个变更持久化到磁盘时被执行一次。正如我们在上一篇文章中描述的那样,Lucene提交的冲洗(flush)操作默认每30分钟执行一次或者当translog变得太大(默认512MB)时执行。在这样的情况下,有可能失去2个Lucene提交之间的所有变更。为了避免这种问题,Elasticsearch采用了translog。所有索引/删除/更新操作被写入到translog,在每个索引/删除/更新操作执行之后(默认情况下是每5秒),translog会被同步以确保变更被持久化。translog被同步到主分片和副本之后,客户端才会收到写请求的确认。

在两次Lucene提交之间发生硬件故障的情况下,可以通过重放translog来恢复自最后一次Lucene提交前的任何丢失的变更,所有的变更将会被索引所接受。

注意:建议在重启Elasticsearch实例之前显式地执行冲洗translog,这样启动会更快,因为要重放的translog被清空。POST /_all/_flush命令可用于冲洗集群中的所有索引。

使用translog的冲洗操作,在文件系统缓存中的段被提交到磁盘,使索引中的变更持久化。现在让我们来看看Lucene的段。

Lucene的段

Lucene索引是由多个段组成,段本身是一个功能齐全的倒排索引。段是不可变的,允许Lucene将新的文档增量地添加到索引中,而不用从头重建索引。对于每一个搜索请求而言,索引中的所有段都会被搜索,并且每个段会消耗CPU的时钟周、文件句柄和内存。这意味着段的数量越多,搜索性能会越低。

为了解决这个问题,Elasticsearch会合并小段到一个较大的段(如下图所示),提交新的合并段到磁盘,并删除那些旧的小段。

这会在后台自动执行而不中断索引或者搜索。由于段合并会耗尽资源,影响搜索性能,Elasticsearch会节制合并过程,为搜索提供足够的可用资源。

接下来有什么?

从搜索请求角度来说,一个Elasticsearch索引中给定分片内的所有Lucene段都会被搜索,但是,从Elasticsearch集群角度而言,获取所有匹配的文档或者深入有序结果文档是有害的。在本系列的后续文章中我们将揭晓原因,让我们来看一下接下来的主题,内容包括了一些在Elasticsearch中为相关性搜索结果的低延迟所做的权衡。

  • Elasticsearch准实时性方面的内容
  • 为什么搜索中的深层分页是有害的?
  • 搜索相关性计算中的权衡之道

查看原文地址:Anatomy of an Elasticsearch Cluster: Part II

posted @ 2016-08-13 11:26 小马歌 阅读(196) | 评论 (0)编辑 收藏
 
from:http://www.infoq.com/cn/articles/anatomy-of-an-elasticsearch-cluster-part03


剖析Elasticsearch集群系列涵盖了当今最流行的分布式搜索引擎Elasticsearch的底层架构和原型实例。本文是这个系列的第三篇,我们将讨论Elasticsearch是如何提供近实时搜索并权衡搜索相关性计算的。

本系列已经得到原文著者Ronak Nathani的授权

在本系列的前一篇中,我们讨论了Elastisearch如何解决分布式系统中的一些基本挑战。在本文中,我们将探讨Elasticsearch在近实时搜索及其权衡计算搜索相关性方面的内容,Insight Data的工程师们已经在使用Elasticsearch构建的数据平台之上,对此有所实践。我将在本文中主要讲述:

近实时搜索

虽然Elasticsearch中的变更不能立即可见,它还是提供了一个近实时的搜索引擎。如前一篇中所述,提交Lucene的变更到磁盘是一个代价昂贵的操作。为了避免在文档对查询依然有效的时候,提交变更到磁盘,Elasticsearch在内存缓冲和磁盘之间提供了一个文件系统缓存。内存缓存(默认情况下)每1秒刷新一次,在文件系统缓存中使用倒排索引创建一个新的段。这个段是开放的并对搜索有效。

文件系统缓存可以拥有文件句柄,文件可以是开放的、可读的或者是关闭的,但是它存在于内存之中。因为刷新间隔默认是1秒,变更不能立即可见,所以说是近实时的。因为translog是尚未落盘的变更持久化记录,它能有助于CRUD操作方面的近实时性。对于每次请求来说,在查找相关段之前,任何最近的变更都能从translog搜索到,因此客户端可以访问到所有的近实时变更。

你可以在创建/更新/删除操作后显式地刷新索引,使变更立即可见,但我并不推荐你这样做,因为这样会创建出来非常多的小segment而影响搜索性能。对于每次搜索请求来说,给定Elasticsearch索引分片中的全部Lucene段都会被搜索到,但是,对于Elasticsearch来说,获取全部匹配的文档或者很深结果页的文档是有害的。让我们来一起看看为什么是这样。

为什么深层分页在分布式搜索中是有害的?

当我们的一次搜索请求在Elasticsearch中匹配了很多的文档,默认情况下,返回的第一页只包含前10条结果。search API提供了fromsize参数,用于指定对于匹配搜索的全部文档,要返回多深的结果。举例来说,如果我们想看到匹配搜索的文档中,排名为5060之间的文档,可以设置from=50size=10。当每个分片接收到这个搜索请求后,各自会创建一个容量为from+size的优先队列来存储该分片上的搜索结果,然后将结果返回给协调节点。

如果我们想看到排名为50,00050,010的结果,那么每个分片要创建一个容量为50,010的优先队列来存储结果,而协调节点要在内存中对数量为shards * 50,010的结果进行排序。这个级别的分页有可能得到结果,也有可以无法实现,这取决于我们的硬件资源,但是这足以说明,我们得非常小心地使用深分页,因为这非常容易使我们的集群崩溃。

一种获取全部匹配结果文档的可行性方案是使用scroll API,它的角色更像关系数据库中的游标。使用scroll API无法进行排序,每个分片只要有匹配搜索的文档,就会持续发送结果给协调节点。

获取大量文档的时候,对结果进行得分排序会非常昂贵。并且由于Elasticsearch是分布式系统,为每个文档计算搜索相关性得分是非常昂贵的。现在,让我们一起看看计算搜索相关性的诸多权衡中的一种。

计算搜索相关性中的权衡

Elasticsearch使用tf-idf来计算搜索相关性。由于其分布式的性质,计算全局的idf(inverse document frequency,逆文档频率)非常昂贵。反之可以这样,每个分片计算本地的idf并将相关性得分分配给结果文档,返回的结果只关乎该分片上的文档。同样地,所有分片使用本地idf计算的相关性得分,返回结果文档,协调节点对所有结果排序并返回前几条。这样做在大多数情况下是没有问题的,除非索引的关键字词项有倾斜或者单个分片上没有代表全局的足够数据。

比如说,如果我们搜索“insight”这个词,但包含"insight"这个词项的大多数文档都存放在一个分片上,这样以来匹配查询的文档将不能公平地在每个分片上进行排序,因为每个分片上的本地idf的值非常不同,得到的搜索结果可能不会非常相关。同样地,如果没有足够的数据,那么对于某些搜索而言,本地idf的值可能大有不同,结果也会不如预期相关。在有足够数据的真实场景中,本地idf值一般会趋于均等,搜索结果是相关的,因为文档得到了公平的得分。

这里有2种应对本地idf得分的办法,但都不建议真正在生产环境中使用。

  • 一种办法是一索引一分片,本地idf即是全局idf,但这没有为并行计算/水平伸缩留有余地,对于大型索引并不实用。
  • 另一种办法是在搜索请求中使用dfs_query_then_search (dfs = distributed frequency search,分布式频率搜索) 参数,这样以来,会首先计算每个分片的本地idf,然后综合这些本地idf的值来计算整个索引的全局idf值,最后使用全局idf计算相关性得分来返回结果。这种方式不为生产环境推荐,因为有足够的数据确保词项频率分布均匀。

在本系列的过去几篇中,我们回顾了一些Elasticsearch的基本原则,对于我们理解并上手Elasticsearch,这些内容非常重要。在接下来的一篇中,我将使用Apache Spark来研究Elasticsearch中的索引数据。

查看英文原文:Anatomy of an Elasticsearch Cluster: Part III

posted @ 2016-08-13 11:26 小马歌 阅读(240) | 评论 (0)编辑 收藏
 
from:http://www.infoq.com/cn/articles/analysis-of-elasticsearch-cluster-part01

剖析Elasticsearch集群系列涵盖了当今最流行的分布式搜索引擎Elasticsearch的底层架构和原型实例。

本文是这个系列的第一篇,在本文中,我们将讨论的Elasticsearch的底层存储模型及CRUD(创建、读取、更新和删除)操作的工作原理。

本系列已经得到原文著者Ronak Nathani的授权

Elasticsearch是当今最流行的分布式搜索引擎,GitHub、 SalesforceIQ、Netflix等公司将其用于全文检索和分析应用。在Insight,我们用到了Elasticsearch的诸多不同功能,比如:

  • 全文检索
    • 比如找到与搜索词项(term)最相关的维基百科文章。
  • 聚合
    • 比如在广告网络中,可视化的搜索词项的竞价直方图。
  • 地理空间API
    • 比如在顺风车平台,匹配最近的司机和乘客。

正是因为Elasticsearch如此流行并且就在我们身边,我决定深入研究一下。本文,我将分享Elasticsearch的存储模型和CRUD操作的工作原理。

当我在思考分布式系统是如何工作时,我脑海里的图案是这样的:

水面以上的是API,以下的才是真正的引擎,一切魔幻般的事件都发生在水下。本文所关注的就是水下的部分,我们将关注:

  • Elasticsearch是主从架构还是无主架构
  • Elasticsearch的存储模型是什么样的
  • Elasticsearch是怎么执行写操作的
  • Elasticsearch是怎么执行读操作的
  • 如何定义搜索结果的相关性

在我们深入这些概念之前,让我们熟悉下相关的术语。

1 辨析Elasticsearch的索引与Lucene的索引

Elasticsearch中的索引是组织数据的逻辑空间(就好比数据库)。1个Elasticsearch的索引有1个或者多个分片(默认是5个)。分片对应实际存储数据的Lucene的索引,分片自身就是一个搜索引擎。每个分片有0或者多个副本(默认是1个)。Elasticsearch的索引还包含"type"(就像数据库中的表),用于逻辑上隔离索引中的数据。在Elasticsearch的索引中,给定一个type,它的所有文档会拥有相同的属性(就像表的schema)。

(点击放大图像)

图a展示了一个包含3个分片的Elasticsearch索引,每个分片拥有1个副本。这些分片组成了一个Elasticsearch索引,每个分片自身是一个Lucene索引。图b展示了Elasticsearch索引、分片、Lucene索引和文档之间的逻辑关系。

对应于关系数据库术语

Elasticsearch Index == Database  Types == Tables  Properties == Schema

现在我们熟悉了Elasticsearch世界的术语,接下来让我们看一下节点有哪些不同的角色。

2 节点类型

一个Elasticsearch实例是一个节点,一组节点组成了集群。Elasticsearch集群中的节点可以配置为3种不同的角色:

  • 主节点:控制Elasticsearch集群,负责集群中的操作,比如创建/删除一个索引,跟踪集群中的节点,分配分片到节点。主节点处理集群的状态并广播到其他节点,并接收其他节点的确认响应。

    每个节点都可以通过设定配置文件elasticsearch.yml中的node.master属性为true(默认)成为主节点。

    对于大型的生产集群来说,推荐使用一个专门的主节点来控制集群,该节点将不处理任何用户请求。

  • 数据节点:持有数据和倒排索引。默认情况下,每个节点都可以通过设定配置文件elasticsearch.yml中的node.data属性为true(默认)成为数据节点。如果我们要使用一个专门的主节点,应将其node.data属性设置为false

  • 客户端节点:如果我们将node.master属性和node.data属性都设置为false,那么该节点就是一个客户端节点,扮演一个负载均衡的角色,将到来的请求路由到集群中的各个节点。

Elasticsearch集群中作为客户端接入的节点叫协调节点。协调节点会将客户端请求路由到集群中合适的分片上。对于读请求来说,协调节点每次会选择不同的分片处理请求,以实现负载均衡。

在我们开始研究发送给协调节点的CRUD请求是如何在集群中传播并被引擎执行之前,让我们先来看一下Elasticsearch内部是如何存储数据,以支持全文检索结果的低延迟服务的。

存储模型

Elasticsearch使用了Apache Lucene,后者是Doug Cutting(Apache Hadoop之父)使用Java开发的全文检索工具库,其内部使用的是被称为倒排索引的数据结构,其设计是为全文检索结果的低延迟提供服务。文档是Elasticsearch的数据单位,对文档中的词项进行分词,并创建去重词项的有序列表,将词项与其在文档中出现的位置列表关联,便形成了倒排索引。

这和一本书后面的索引非常类似,即书中包含的词汇与其出现的页码列表关联。当我们说文档被索引了,我们指的是倒排索引。我们来看下如下2个文档是如何被倒排索引的:

文档1(Doc 1): Insight Data Engineering Fellows Program
文档2(Doc 2): Insight Data Science Fellows Program

如果我们想找包含词项"insight"的文档,我们可以扫描这个(单词有序的)倒排索引,找到"insight"并返回包含改词的文档ID,示例中是Doc 1和Doc 2。

为了提高可检索性(比如希望大小写单词都返回),我们应当先分析文档再对其索引。分析包括2个部分:

  • 将句子词条化为独立的单词
  • 将单词规范化为标准形式

默认情况下,Elasticsearch使用标准分析器,它使用了:

  • 标准分词器以单词为界来切词
  • 小写词条(token)过滤器来转换单词

还有很多可用的分析器在此不列举,请参考相关文档。

为了实现查询时能得到对应的结果,查询时应使用与索引时一致的分析器,对文档进行分析。

注意:标准分析器包含了停用词过滤器,但默认情况下没有启用。

现在,倒排索引的概念已经清楚,让我们开始CRUD操作的研究吧。我们从写操作开始。

剖析写操作

创建((C)reate)

当我们发送索引一个新文档的请求到协调节点后,将发生如下一组操作:

  • Elasticsearch集群中的每个节点都包含了改节点上分片的元数据信息。协调节点(默认)使用文档ID参与计算,以便为路由提供合适的分片。Elasticsearch使用MurMurHash3函数对文档ID进行哈希,其结果再对分片数量取模,得到的结果即是索引文档的分片。

    shard = hash(document_id) % (num_of_primary_shards)
  • 当分片所在的节点接收到来自协调节点的请求后,会将该请求写入translog(我们将在本系列接下来的文章中讲到),并将文档加入内存缓冲。如果请求在主分片上成功处理,该请求会并行发送到该分片的副本上。当translog被同步(fsync)到全部的主分片及其副本上后,客户端才会收到确认通知。
  • 内存缓冲会被周期性刷新(默认是1秒),内容将被写到文件系统缓存的一个新段上。虽然这个段并没有被同步(fsync),但它是开放的,内容可以被搜索到。
  • 每30分钟,或者当translog很大的时候,translog会被清空,文件系统缓存会被同步。这个过程在Elasticsearch中称为冲洗(flush)。在冲洗过程中,内存中的缓冲将被清除,内容被写入一个新段。段的fsync将创建一个新的提交点,并将内容刷新到磁盘。旧的translog将被删除并开始一个新的translog。

下图展示了写请求及其数据流。

(点击放大图像)

更新((U)pdate)和删除((D)elete)

删除和更新也都是写操作。但是Elasticsearch中的文档是不可变的,因此不能被删除或者改动以展示其变更。那么,该如何删除和更新文档呢?

磁盘上的每个段都有一个相应的.del文件。当删除请求发送后,文档并没有真的被删除,而是在.del文件中被标记为删除。该文档依然能匹配查询,但是会在结果中被过滤掉。当段合并(我们将在本系列接下来的文章中讲到)时,在.del文件中被标记为删除的文档将不会被写入新段。

接下来我们看更新是如何工作的。在新的文档被创建时,Elasticsearch会为该文档指定一个版本号。当执行更新时,旧版本的文档在.del文件中被标记为删除,新版本的文档被索引到一个新段。旧版本的文档依然能匹配查询,但是会在结果中被过滤掉。

文档被索引或者更新后,我们就可以执行查询操作了。让我们看看在Elasticsearch中是如何处理查询请求的。

剖析读操作((R)ead)

读操作包含2部分内容:

  • 查询阶段
  • 提取阶段

我们来看下每个阶段是如何工作的。

查询阶段

在这个阶段,协调节点会将查询请求路由到索引的全部分片(主分片或者其副本)上。每个分片独立执行查询,并为查询结果创建一个优先队列,以相关性得分排序(我们将在本系列的后续文章中讲到)。全部分片都将匹配文档的ID及其相关性得分返回给协调节点。协调节点创建一个优先队列并对结果进行全局排序。会有很多文档匹配结果,但是,默认情况下,每个分片只发送前10个结果给协调节点,协调节点为全部分片上的这些结果创建优先队列并返回前10个作为hit。

提取阶段

当协调节点在生成的全局有序的文档列表中,为全部结果排好序后,它将向包含原始文档的分片发起请求。全部分片填充文档信息并将其返回给协调节点。

下图展示了读请求及其数据流。

(点击放大图像)

如上所述,查询结果是按相关性排序的。接下来,让我们看看相关性是如何定义的。

搜索相关性

相关性是由搜索结果中Elasticsearch打给每个文档的得分决定的。默认使用的排序算法是tf/idf(词频/逆文档频率)。词频衡量了一个词项在文档中出现的次数 (频率越高 == 相关性越高),逆文档频率衡量了词项在全部索引中出现的频率,是一个索引中文档总数的百分比(频率越高 == 相关性越低)。最后的得分是tf-idf得分与其他因子比如(短语查询中的)词项接近度、(模糊查询中的)词项相似度等的组合。

接下来有什么?

这些CRUD操作由Elasticsearch内部的一些数据结构所支持,这对于理解Elasticsearch的工作机制非常重要。在接下来的系列文章中,我将带大家走进类似的那些概念并告诉大家在使用Elasticsearch中有哪些坑。

  • Elasticsearch中的脑裂问题及防治措施
  • 事务日志
  • Lucene的段
  • 为什么搜索时使用深层分页很危险
  • 计算搜索相关性中困难及权衡
  • 并发控制
  • 为什么Elasticsearch是准实时的
  • 如何确保读和写的一致性

查看原文地址:http://insightdataengineering.com/blog/elasticsearch-crud

posted @ 2016-08-13 11:15 小马歌 阅读(237) | 评论 (0)编辑 收藏
 
from:http://www.infoq.com/cn/articles/what-complete-micro-service-system-should-include?utm_source=infoq&utm_medium=popular_widget&utm_campaign=popular_content_list&utm_content=homepage


近几年,微服务架构迅速在整个技术社区窜红,它被认为是IT软件架构的未来方向,大神Martin Fowler也给微服务极高的评价。那为什么我们需要微服务,微服务的真正优势到底是什么,一个完整的微服务系统,应该包含哪些功能,本文作者刘彦夫在软件设计和开发领域有10多年工作经验,他将会从他的角度给出答案。

对微服务的基本理解

顾名思义,微服务要从两个方面来理解,一个是“微”,一个是“服务”。体型小到一定程度才能叫“微”,这个程度是什么呢?一个身高1米6,体重90斤的MM,我们说她苗条。微服务也一样,根据亚马逊CEO Bezos给出的有趣定义,单个微服务的设计、开发、测试和运维的所有人加在一起吃饭,只需要两个批萨就够了,这是就是著名的two pizza team rule。

具备什么样的能力才能算是“服务”?这个话题很大,我这里按照自己的片面理解总结一下,所谓服务就一定会区别于系统的功能,服务是一个或者一组相对的较小且独立的功能单元,是用户可以感知的功能最小集,比如:购物车,订单,信用卡结算等都可以作为单个服务独立提供。

这个理解显然不够深刻,为了进一步理解为什么微服务在近两年业界迅速窜红,理解为什么微服务会被认为是IT软件架构的未来方向,就要理解为什么我们需要微服务?它能给企业带来什么价值。传统企业的IT软件大多都是各种独立系统的堆砌,这些系统的问题总结来说就是扩展性差,可靠性不高,维护成本高。后来有了一个叫SOA的软件架构专门针对这些问题给出了一套解决方案,很多企业也因此将自身IT系统迁移到SOA架构上。

但是,由于SOA早期均使用了总线模式,这种总线模式是与某种技术栈强绑定的,比如:J2EE。这导致很多企业的遗留系统很难对接,切换时间太长,成本太高,新系统稳定性的收敛也需要一些时间。最终SOA开起来很美,但却成为了企业级奢侈品,中小公司都望而生畏。

依然SOA

微服务,从本质意义上看,还是SOA架构。但内涵有所不同,微服务并不绑定某种特殊的技术,在一个微服务的系统中,可以有Java编写的服务,也可以有Python编写的服务,他们是靠Restful架构风格统一成一个系统的。

最粗浅的理解就是将微服务之间的交互看作是各种字符串的传递,各种语言都可以很好的处理字符串,所以微服务本身与具体技术实现无关,扩展性强。另一个不同是微服务架构本身很轻,底层也有类似于SOA的总线,不过非常轻薄,现在看到的就两种方式:MQ和HTTP,而HTTP都不能完全等同于总线,而仅仅是个信息通道。

所以,基于这种简单的的协议规范,无论是兼容老旧系统,还是上线新业务,都可以随着时代的步伐,滚动升级。比如:你去年还在使用.NET技术,今年就可以平滑的过度到Go了,而且系统已有服务不用改动。所以微服务架构,既保护用户已有投资,又很容易向新技术演进。

微服务水下的冰山

人月不是银弹,微服务更不是银弹,好像软件微服务化了,软件系统就能够应对各种问题了。其实微服务的水面下藏着巨大的冰山。下面是微服务提供的能力,以及背后需要付出的代价。

  1. 单个微服务代码量小,易修改和维护。但是,系统复杂度的总量是不变的,每个服务代码少了,但服务的个数肯定就多了。就跟拼图游戏一样,切的越碎,越难拼出整幅图。一个系统被拆分成零碎的微服务,最后要集成为一个完整的系统,其复杂度肯定比大块的功能集成要高很多。

  2. 单个微服务数据独立,可独立部署和运行。虽然微服务本身是可以独立部署和运行的,但仍然避免不了业务上的你来我往,这就涉及到要对外通信,当微服务的数量达到一定量级的时候,如何提供一个高效的集群通信机制成为一个问题。

  3. 单个微服务拥有自己的进程,进程本身就可以动态的启停,为无缝升级的打好了基础,但谁来启动和停止进程,什么时机,选择在哪台设备上做这件事情才是无缝升级的关键。这个能力并不是微服务本身提供的,而是需要背后强大的版本管理和部署能力。

  4. 多个相同的微服务可以做负载均衡,提高性能和可靠性。正是因为相同微服务可以有多个不同实例,让服务按需动态伸缩成为可能,在高峰期可以启动更多的相同的微服务实例为更多用户服务,以此提高响应速度。同时这种机制也提供了高可靠性,在某个微服务故障后,其他相同的微服务可以接替其工作,对外表现为某个设备故障后业务不中断。同样的道理,微服务本身是不会去关心系统负载的,那么什么时候应该启动更多的微服务,多个微服务的流量应该如何调度和分发,这背后也有一套复杂的负载监控和均衡的系统在起作用。

  5. 微服务可以独立部署和对外提供服务,微服务的业务上线和下线是动态的,当一个新的微服务上线时,用户是如何访问到这种新的服务?这就需要有一个统一的入口,新的服务可以动态的注册到这个入口上,用户每次访问时可以从这个入口拿到系统所有服务的访问地址,类似于到餐厅吃饭,新菜要写到“菜单”中,以供用户选择。这个统一的系统入口并不是微服务本身的一部分,所以这种能力需要系统单独提供。

  6. 还有一些企业级关注的系统问题,比如,安全策略如何集中管理?系统故障如何快速审计和跟踪到具体服务?整个系统状态如何监控?服务之间的依赖关系如何管理?等等这些问题都不是单个微服务考虑的范畴,而需要有一个系统性的考虑和设计,让每个微服务都能够按照系统性的要求和约束提供对应的安全性,可靠性,可维护性的能力。

综上所述,微服务关键其实不仅仅是微服务本身,而是系统要提供一套基础的架构,这种架构使得微服务可以独立的部署、运行、升级,不仅如此,这个系统架构还让微服务与微服务之间在结构上“松耦合”,而在功能上则表现为一个统一的整体。这种所谓的“统一的整体”表现出来的是统一风格的界面,统一的权限管理,统一的安全策略,统一的上线过程,统一的日志和审计方法,统一的调度方式,统一的访问入口等等。

这些系统性的功能也需要有一些服务来提供,这些服务不会直接呈现给最终用户,也就是微服务系统冰山下面的部分,我们可以简称它为微服务系统的“底座”。所有的微服务都像一个APP,插在这个底座的上面,享受这个底座提供的系统能力比如:元数据存放、灰度发布、蓝绿部署等等。

微服务系统底座

一个完整的微服务系统,它的底座最少要包含以下功能:

  • 日志和审计,主要是日志的汇总,分类和查询

  • 监控和告警,主要是监控每个服务的状态,必要时产生告警

  • 消息总线,轻量级的MQ或HTTP

  • 注册发现

  • 负载均衡

  • 部署和升级

  • 事件调度机制

  • 资源管理,如:底层的虚拟机,物理机和网络管理

以下功能不是最小集的一部分,但也属于底座功能:

  • 认证和鉴权

  • 微服务统一代码框架,支持多种编程语言

  • 统一服务构建和打包

  • 统一服务测试

  • 微服务CI/CD流水线

  • 服务依赖关系管理

  • 统一问题跟踪调试框架,俗称调用链

  • 灰度发布

  • 蓝绿部署

    • 令人困惑的几个问题

    微服务的底座是不是必须的?

    是的,基本上是必须的。你可以不用代码实现一个资源管理服务,可以手工用Excel管理你的所有机器资源,但是不代表微服务系统没有这个功能,只不过这个功能是人工实现的。再举个例子,日志系统如果只是简单的打印文件,那么多个微服务的日志就需要手工收集,人工分类和筛选。所以,微服务的底座最小集一定会存在,问题是看怎样实现它。

    这里仅仅是总结了对微服务系统的基本理解,而实现这个架构有很多技术,这里不进行详细展开。实践方面,推荐王磊的《微服务架构与实践》,他描述了使用Ruby相关的技术实现了一整套微服务系统,特别是书中后面的实践部分讲解了如何将已有的系统演化为微服务架构,是很好的参考和指导材料。

    是不是所有软件都能做微服务?

    这个命题有些微妙,也很难说清楚,回答这个命题本身就是一种挑战,可能最终也没有正确答案。不过,我还是把我自己的理解写在这里,让大家去拍砖。在我这里,答案是否定的。我只需举出一个反例,比如:存储系统,其架构是传统的分层架构,每一层都使用下面一层的服务,并为上一层提供服务。虽然可以将这种架构调整为基于服务的架构,但没办法做成微服务。

    区别在哪里呢?核心的区别在于独立性上,微服务大多是可以独立的运行和使用的,而存储这种非常底层和基础的系统,每层部件都不能单独被使用,比如:Pool管理、CHUNK管理、VOL管理、NFS文件系统,这些功能都无法离开另外一些功能而独立运行,要对外提供可用的存储功能,一大堆功能必须一起上。这种系统做到极致,最多也就能够使其部件可以独立的部署和升级,俗称打热补丁。

    这也就是为什么这种底层传统系统架构通常是单块架构的原因。由于单块架构的各个部分调用关系紧密,做成微服务后系统集成成本会大大增加,不仅如此,这样的架构做成微服务并不能提高交付效率,因为各个部分根本就无法独立的运行和测试。

    什么样的软件做成微服务?

    能不能做成微服务,取决于四个要素:

    • 小:微服务体积小,2 pizza团队。

    • 独:能够独立的部署和运行。

    • 轻:使用轻量级的通信机制和架构。

    • 松:为服务之间是松耦合的。

    针对于小、轻、松都是可以通过某些技术手段达到目的,而独立的部署和运行,则是和业务本身有关系,如果你这个系统提供的业务是贴近最终用户的,并且这些功能之间的耦合性很小,则微服务就可以按照业务功能本身的独立性来划分,则这类系统做成微服务是非常合适的。如果系统提供的业务是非常底层的,如:操作系统内核、存储系统、网络系统、数据库系统等等,这类系统都偏底层,功能和功能之间有着紧密的配合关系,如果强制拆分为较小的服务单元,会让集成工作量急剧上升,并且这种人为的切割无法带来业务上的真正的隔离,所以无法做到独立部署和运行,也就更加无法做到真正的微服务了。


    感谢郭蕾对本文的审校。

    给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ@丁晓昀),微信(微信号:InfoQChina)关注我们。

    posted @ 2016-08-13 10:53 小马歌 阅读(213) | 评论 (0)编辑 收藏
     
    作者:MagiLu
    链接:https://zhuanlan.zhihu.com/p/20308548
    来源:知乎
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    作者:johnczchen

    出品:QQ空间终端开发团队

    原文发布于QQ空间终端开发团队的官方公众号,任何形式的转载之前必须与本人联系。


    1.背景

    当一个App发布之后,突然发现了一个严重bug需要进行紧急修复,这时候公司各方就会忙得焦头烂额:重新打包App、测试、向各个应用市场和渠道换包、提示用户升级、用户下载、覆盖安装。有时候仅仅是为了修改了一行代码,也要付出巨大的成本进行换包和重新发布。

    这时候就提出一个问题:有没有办法以补丁的方式动态修复紧急Bug,不再需要重新发布App,不再需要用户重新下载,覆盖安装?

    虽然Android系统并没有提供这个技术,但是很幸运的告诉大家,答案是:可以,我们QQ空间提出了热补丁动态修复技术来解决以上这些问题。

    2.实际案例

    空间Android独立版5.2发布后,收到用户反馈,结合版无法跳转到独立版的访客界面,每天都较大的反馈。在以前只能紧急换包,重新发布。成本非常高,也影响用户的口碑。最终决定使用热补丁动态修复技术,向用户下发Patch,在用户无感知的情况下,修复了外网问题,取得非常好的效果。

    3.解决方案

    该方案基于的是android dex分包方案的,关于dex分包方案,网上有几篇解释了,所以这里就不再赘述,具体可以看这里Android dex分包方案

    简单的概括一下,就是把多个dex文件塞入到app的classloader之中,但是android dex拆包方案中的类是没有重复的,如果classes.dex和classes1.dex中有重复的类,当用到这个重复的类的时候,系统会选择哪个类进行加载呢?

    让我们来看看类加载的代码:

    一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。

    理论上,如果在不同的dex中有相同的类存在,那么会优先选择排在前面的dex文件的类,如下图:

    在此基础上,我们构想了热补丁的方案,把有问题的类打包到一个dex(patch.dex)中去,然后把这个dex插入到Elements的最前面,如下图:

    好,该方案基于第二个拆分dex的方案,方案实现如果懂拆分dex的原理的话,大家应该很快就会实现该方案,如果没有拆分dex的项目的话,可以参考一下谷歌的multidex方案实现。然后在插入数组的时候,把补丁包插入到最前面去。

    好,看似问题很简单,轻松的搞定了,让我们来试验一下,修改某个类,然后打包成dex,插入到classloader,当加载类的时候出现了(本例中是QzoneActivityManager要被替换):

    为什么会出现以上问题呢?

    从log的意思上来讲,ModuleManager引用了QzoneActivityManager,但是发现这这两个类所在的dex不在一起,其中:

    1. ModuleManager在classes.dex中

    2. QzoneActivityManager在patch.dex中

    结果发生了错误。

    这里有个问题,拆分dex的很多类都不是在同一个dex内的,怎么没有问题?

    让我们搜索一下抛出错误的代码所在,嘿咻嘿咻,找到了一下代码:

    从代码上来看,如果两个相关联的类在不同的dex中就会报错,但是拆分dex没有报错这是为什么,原来这个校验的前提是:

    如果引用者(也就是ModuleManager)这个类被打上了CLASS_ISPREVERIFIED标志,那么就会进行dex的校验。那么这个标志是什么时候被打上去的?

    让我们在继续搜索一下代码,嘿咻嘿咻~~,在DexPrepare.cpp找到了一下代码:

    这段代码是dex转化成odex(dexopt)的代码中的一段,我们知道当一个apk在安装的时候,apk中的classes.dex会被虚拟机(dexopt)优化成odex文件,然后才会拿去执行。

    虚拟机在启动的时候,会有许多的启动参数,其中一项就是verify选项,当verify选项被打开的时候,上面doVerify变量为true,那么就会执行dvmVerifyClass进行类的校验,如果dvmVerifyClass校验类成功,那么这个类会被打上CLASS_ISPREVERIFIED的标志,那么具体的校验过程是什么样子的呢?

    此代码在DexVerify.cpp中,如下:

    1. 验证clazz->directMethods方法,directMethods包含了以下方法:

    1. static方法

    2. private方法

    3. 构造函数

    2. clazz->virtualMethods

    1. 虚函数=override方法?

    概括一下就是如果以上方法中直接引用到的类(第一层级关系,不会进行递归搜索)和clazz都在同一个dex中的话,那么这个类就会被打上CLASS_ISPREVERIFIED

    所以为了实现补丁方案,所以必须从这些方法中入手,防止类被打上CLASS_ISPREVERIFIED标志。

    最终空间的方案是往所有类的构造函数里面插入了一段代码,代码如下:

    if (ClassVerifier.PREVENT_VERIFY) {

    System.out.println(AntilazyLoad.class);

    }

    其中AntilazyLoad类会被打包成单独的hack.dex,这样当安装apk的时候,classes.dex内的类都会引用一个在不相同dex中的AntilazyLoad类,这样就防止了类被打上CLASS_ISPREVERIFIED的标志了,只要没被打上这个标志的类都可以进行打补丁操作。

    然后在应用启动的时候加载进来.AntilazyLoad类所在的dex包必须被先加载进来,不然AntilazyLoad类会被标记为不存在,即使后续加载了hack.dex包,那么他也是不存在的,这样屏幕就会出现茫茫多的类AntilazyLoad找不到的log。

    所以Application作为应用的入口不能插入这段代码。(因为载入hack.dex的代码是在Application中onCreate中执行的,如果在Application的构造函数里面插入了这段代码,那么就是在hack.dex加载之前就使用该类,该类一次找不到,会被永远的打上找不到的标志)

    其中:

    之所以选择构造函数是因为他不增加方法数,一个类即使没有显式的构造函数,也会有一个隐式的默认构造函数。

    空间使用的是在字节码插入代码,而不是源代码插入,使用的是javaassist库来进行字节码插入的。

    隐患:

    虚拟机在安装期间为类打上CLASS_ISPREVERIFIED标志是为了提高性能的,我们强制防止类被打上标志是否会影响性能?这里我们会做一下更加详细的性能测试.但是在大项目中拆分dex的问题已经比较严重,很多类都没有被打上这个标志。

    如何打包补丁包:

    1. 空间在正式版本发布的时候,会生成一份缓存文件,里面记录了所有class文件的md5,还有一份mapping混淆文件。

    2. 在后续的版本中使用-applymapping选项,应用正式版本的mapping文件,然后计算编译完成后的class文件的md5和正式版本进行比较,把不相同的class文件打包成补丁包。

    备注:该方案现在也应用到我们的编译过程当中,编译不需要重新打包dex,只需要把修改过的类的class文件打包成patch dex,然后放到sdcard下,那么就会让改变的代码生效。



    关于Qzone :

    Qzone 是中国最大的社交网络,是腾讯集团的核心平台之一,目前Qzone的月活跃账户数达到6.68亿,Qzone智能终端月活跃账户数达到5.68亿。从2005~2015,Qzone见证了国内互联网蓬勃发展的十年,这十年风云变幻,但我们的业务却不断向前发展,也希望更多的朋友能够加入我们,共同迎接互联网和Qzone的下一个十年。

    posted @ 2016-07-07 16:11 小马歌 阅读(213) | 评论 (0)编辑 收藏
    仅列出标题
    共95页: First 上一页 2 3 4 5 6 7 8 9 10 下一页 Last