月度归档:2016年07月

Elasticsearch 架构以及源码概览

Elasticsearch 是最近两年异军突起的一个兼有搜索引擎和NoSQL数据库功能的开源系统,基于Java/Lucene构建。最近研究了一下,感觉 Elasticsearch 的架构以及其开源的生态构建都有许多可借鉴之处,所以整理成文章分享下。本文的代码以及架构分析主要基于 Elasticsearch 2.X 最新稳定版。

Elasticsearch 看名字就能大概了解下它是一个弹性的搜索引擎。首先弹性隐含的意思是分布式,单机系统是没法弹起来的,然后加上灵活的伸缩机制,就是这里的 Elastic 包含的意思。它的搜索存储功能主要是 Lucene 提供的,Lucene 相当于其存储引擎,它在之上封装了索引,查询,以及分布式相关的接口。

Elasticsearch 中的几个概念

  1. 集群(Cluster)一组拥有共同的 cluster name 的节点。
  2. 节点(Node) 集群中的一个 Elasticearch 实例。
  3. 索引(Index) 相当于关系数据库中的database概念,一个集群中可以包含多个索引。这个是个逻辑概念。
  4. 主分片(Primary shard) 索引的子集,索引可以切分成多个分片,分布到不同的集群节点上。分片对应的是 Lucene 中的索引。
  5. 副本分片(Replica shard)每个主分片可以有一个或者多个副本。
  6. 类型(Type)相当于数据库中的table概念,mapping是针对 Type 的。同一个索引里可以包含多个 Type。
  7. Mapping 相当于数据库中的schema,用来约束字段的类型,不过 Elasticsearch 的 mapping 可以自动根据数据创建。
  8. 文档(Document) 相当于数据库中的row。
  9. 字段(Field)相当于数据库中的column。
  10. 分配(Allocation) 将分片分配给某个节点的过程,包括分配主分片或者副本。如果是副本,还包含从主分片复制数据的过程。

分布式以及 Elastic

分布式系统要解决的第一个问题就是节点之间互相发现以及选主的机制。如果使用了 Zookeeper/Etcd 这样的成熟的服务发现工具,这两个问题都一并解决了。但 Elasticsearch 并没有依赖这样的工具,带来的好处是部署服务的成本和复杂度降低了,不用预先依赖一个服务发现的集群,缺点当然是将复杂度带入了 Elasticsearch 内部。

服务发现以及选主 ZenDiscovery

  1. 节点启动后先ping(这里的ping是 Elasticsearch 的一个RPC命令。如果 discovery.zen.ping.unicast.hosts 有设置,则ping设置中的host,否则尝试ping localhost 的几个端口, Elasticsearch 支持同一个主机启动多个节点)
  2. Ping的response会包含该节点的基本信息以及该节点认为的master节点。
  3. 选举开始,先从各节点认为的master中选,规则很简单,按照id的字典序排序,取第一个。
  4. 如果各节点都没有认为的master,则从所有节点中选择,规则同上。这里有个限制条件就是 discovery.zen.minimum_master_nodes,如果节点数达不到最小值的限制,则循环上述过程,直到节点数足够可以开始选举。
  5. 最后选举结果是肯定能选举出一个master,如果只有一个local节点那就选出的是自己。
  6. 如果当前节点是master,则开始等待节点数达到 minimum_master_nodes,然后提供服务。
  7. 如果当前节点不是master,则尝试加入master。

Elasticsearch 将以上服务发现以及选主的流程叫做 ZenDiscovery 。由于它支持任意数目的集群(1-N),所以不能像 Zookeeper/Etcd 那样限制节点必须是奇数,也就无法用投票的机制来选主,而是通过一个规则,只要所有的节点都遵循同样的规则,得到的信息都是对等的,选出来的主节点肯定是一致的。但分布式系统的问题就出在信息不对等的情况,这时候很容易出现脑裂(Split-Brain)的问题,大多数解决方案就是设置一个quorum值,要求可用节点必须大于quorum(一般是超过半数节点),才能对外提供服务。而 Elasticsearch 中,这个quorum的配置就是 discovery.zen.minimum_master_nodes 。 说到这里要吐槽下 Elasticsearch 的方法和变量命名,它的方法和配置中的master指的是master的候选节点,也就是说可能成为master的节点,并不是表示当前的master,我就被它的一个 isMasterNode 方法坑了,开始一直没能理解它的选举规则。

弹性伸缩 Elastic

Elasticsearch 的弹性体现在两个方面: 1. 服务发现机制让节点很容易加入和退出。 2. 丰富的设置以及allocation API。

Elasticsearch 节点启动的时候只需要配置discovery.zen.ping.unicast.hosts,这里不需要列举集群中所有的节点,只要知道其中一个即可。当然为了避免重启集群时正好配置的节点挂掉,最好多配置几个节点。节点退出时只需要调用 API 将该节点从集群中排除 (Shard Allocation Filtering),系统会自动迁移该节点上的数据,然后关闭该节点即可。当然最好也将不可用的已知节点从其他节点的配置中去除,避免下次启动时出错。

分片(Shard)以及副本(Replica)  分布式存储系统为了解决单机容量以及容灾的问题,都需要有分片以及副本机制。Elasticsearch 没有采用节点级别的主从复制,而是基于分片。它当前还未提供分片切分(shard-splitting)的机制,只能创建索引的时候静态设置。

ElasticSearch-01

(elasticsearch 官方博客的图片)

比如上图所示,开始设置为5个分片,在单个节点上,后来扩容到5个节点,每个节点有一个分片。如果继续扩容,是不能自动切分进行数据迁移的。官方文档的说法是分片切分成本和重新索引的成本差不多,所以建议干脆通过接口重新索引

Elasticsearch 的分片默认是基于id 哈希的,id可以用户指定,也可以自动生成。但这个可以通过参数(routing)或者在mapping配置中修改。当前版本默认的哈希算法是MurmurHash3

Elasticsearch 禁止同一个分片的主分片和副本分片在同一个节点上,所以如果是一个节点的集群是不能有副本的。

恢复以及容灾

分布式系统的一个要求就是要保证高可用。前面描述的退出流程是节点主动退出的场景,但如果是故障导致节点挂掉,Elasticsearch 就会主动allocation。但如果节点丢失后立刻allocation,稍后节点恢复又立刻加入,会造成浪费。Elasticsearch的恢复流程大致如下:

  1. 集群中的某个节点丢失网络连接
  2. master提升该节点上的所有主分片的在其他节点上的副本为主分片
  3. cluster集群状态变为 yellow ,因为副本数不够
  4. 等待一个超时设置的时间,如果丢失节点回来就可以立即恢复(默认为1分钟,通过 index.unassigned.node_left.delayed_timeout 设置)。如果该分片已经有写入,则通过translog进行增量同步数据。
  5. 否则将副本分配给其他节点,开始同步数据。

但如果该节点上的分片没有副本,则无法恢复,集群状态会变为red,表示可能要丢失该分片的数据了。

分布式集群的另外一个问题就是集群整个重启后可能导致不预期的分片重新分配(部分节点没有启动完成的时候,集群以为节点丢失),浪费带宽。所以 Elasticsearch 通过以下静态配置(不能通过API修改)控制整个流程,以10个节点的集群为例:

  • gateway.recover_after_nodes: 8
  • gateway.expected_nodes: 10
  • gateway.recover_after_time: 5m

比如10个节点的集群,按照上面的规则配置,当集群重启后,首先系统等待 minimum_master_nodes(6)个节点加入才会选出master, recovery操作是在 master节点上进行的,由于我们设置了 recover_after_nodes(8),系统会继续等待到8个节点加入, 才开始进行recovery。当开始recovery的时候,如果发现集群中的节点数小于expected_nodes,也就是还有部分节点未加入,于是开始recover_after_time 倒计时(如果节点数达到expected_nodes则立刻进行 recovery),5分钟后,如果剩余的节点依然没有加入,则会进行数据recovery。

Elasticsearch 除了支持 Lucene 本身的检索功能外,在之上做了一些扩展。 1. 脚本支持
Elasticsearch 默认支持groovy脚本,扩展了 Lucene 的评分机制,可以很容易的支持复杂的自定义评分算法。它默认只支持通过sandbox方式实现的脚本语言(如lucene expression,mustache),groovy必须明确设置后才能开启。Groovy的安全机制是通过java.security.AccessControlContext设置了一个class白名单来控制权限的,1.x版本的时候是自己做的一个白名单过滤器,但限制策略有漏洞,导致一个远程代码执行漏洞。 2. 默认会生成一个 _all 字段,将所有其他字段的值拼接在一起。这样搜索时可以不指定字段,并且方便实现跨字段的检索。 3. Suggester Elasticsearch 通过扩展的索引机制,可以实现像google那样的自动完成suggestion以及搜索词语错误纠正的suggestion。

NoSQL 数据库

Elasticsearch 可以作为数据库使用,主要依赖于它的以下特性:

  1. 默认在索引中保存原始数据,并可获取。这个主要依赖 Lucene 的store功能。
  2. 实现了translog,提供了实时的数据读取能力以及完备的数据持久化能力(在服务器异常挂掉的情况下依然不会丢数据)。Lucene 因为有 IndexWriter buffer, 如果进程异常挂掉,buffer中的数据是会丢失的。所以 Elasticsearch 通过translog来确保不丢数据。同时通过id直接读取文档的时候,Elasticsearch 会先尝试从translog中读取,之后才从索引中读取。也就是说,即便是buffer中的数据尚未刷新到索引,依然能提供实时的数据读取能力。Elasticsearch 的translog 默认是每次写请求完成后统一fsync一次,同时有个定时任务检测(默认5秒钟一次)。如果业务场景需要更大的写吞吐量,可以调整translog相关的配置进行优化。
  3. dynamic-mapping 以及 schema-free
    Elasticsearch 的dynamic-mapping相当于根据用户提交的数据,动态检测字段类型,自动给数据库表建立表结构,也可以动态增加字段,所以它叫做schema-free,而不是schema-less。这种方式的好处是用户能一定程度享受schema-less的好处,不用提前建立表结构,同时因为实际上是有schema的,可以做查询上的优化,检索效率要比纯schema-less的数据库高许多。但缺点就是已经创建的索引不能变更数据类型(Elasticsearch 写入数据的时候如果类型不匹配会自动尝试做类型转换,如果失败就会报错,比如数字类型的字段写入字符串”123”是可以的,但写入”abc”就不可以。),要损失一定的自由度。另外 Elasticsearch 提供的index-template功能方便用户动态创建索引的时候预先设定索引的相关参数以及type mapping,比如按天创建日志库,template可以设置为对 log-* 的索引都生效。这两个功能我建议新的数据库都可以借鉴下。
  4. 丰富的QueryDSL功能
    Elasticsearch 的query语法基本上和sql对等的,除了join查询,以及嵌套临时表查询不能支持。不过 Elasticsearch 支持嵌套对象以及parent外部引用查询,所以一定程度上可以解决关联查询的需求。另外group by这种查询可以通过其aggregation实现。Elasticsearch 提供的aggregation能力非常强大,其生态圈里的 Kibana 主要就是依赖aggregation来实现数据分析以及可视化的。

系统架构

Elasticsearch 的依赖注入用的是guice,网络使用netty,提供http rest和RPC两种协议。

Elasticsearch 之所以用guice,而不是用spring做依赖注入,关键的一个原因是guice可以帮它很容易的实现模块化,通过代码进行模块组装,可以很精确的控制依赖注入的管理范围。比如 Elasticsearch 给每个shard单独生成一个injector,可以将该shard相关的配置以及组件注入进去,降低编码和状态管理的复杂度,同时删除shard的时候也方便回收相关对象。这方面有兴趣使用guice的可以借鉴。

ClusterState

前面我们分析了 Elasticsearch 的服务发现以及选举机制,它是内部自己实现的。服务发现工具做的事情其实就是跨服务器的状态同步,多个节点修改同一个数据对象,需要有一种机制将这个数据对象同步到所有的节点。Elasticsearch 的ClusterState 就是这样一个数据对象,保存了集群的状态,索引/分片的路由表,节点列表,元数据等,还包含一个ClusterBlocks,相当于分布式锁,用于实现分布式的任务同步。

主节点上有个单独的进程处理 ClusterState 的变更操作,每次变更会更新版本号。变更后会通过PRC接口同步到其他节点。主节知道其他节点的ClusterState 的当前版本,发送变更的时候会做diff,实现增量更新。

Rest 和 RPC

elasticsearch-rest

Elasticsearch 的rest请求的传递流程如上图(这里对实际流程做了简化): 1. 用户发起http请求,Elasticsearch 的9200端口接受请求后,传递给对应的RestAction。 2. RestAction做的事情很简单,将rest请求转换为RPC的TransportRequest,然后调用NodeClient,相当于用客户端的方式请求RPC服务,只不过transport层会对本节点的请求特殊处理。

这样做的好处是将http和RPC两层隔离,增加部署的灵活性。部署的时候既可以同时开启RPC和http服务,也可以用client模式部署一组服务专门提供http rest服务,另外一组只开启RPC服务,专门做data节点,便于分担压力。

Elasticsearch 的RPC的序列化机制使用了 Lucene 的压缩数据类型,支持vint这样的变长数字类型,省略了字段名,用流式方式按顺序写入字段的值。每个需要传输的对象都需要实现:

设计高并发下的读服务?一个电商老兵的10条经验

本文作者是一个一线的电商老兵,任职于京东商城。在本文中,他将会分享他在构建以读为主的系统时总结的经验和教训,内容包括使用HTTP协议对外通讯、使用短连接、数据异构、巧用缓存、流量控制、防刷、降级、多域名等,作者老马不带遮掩的,把自己总结的经验,包括代码都放到这里了,欢迎各位检阅!

几乎所有的互联网系统从开始都是一体化设计的,基本上所有的功能代码都是耦合在一起的。后续随着用户的不断增多业务也越来越多样化,系统需要的维护人员也会越来越多,相应的系统的复杂度、稳定性、可维护性也就越来越难控制,这时系统的拆分以及服务化就成了必然的选择。

系统被拆分后实现方式也就多样化起来,各个系统可以根据自己的业务需求、技术特性、方便程度甚至个人喜好来选择使用不同的语言。服务化后各种功能被拆分的越来越细,原来可能一次请求能够完成的事,现在就需要多次请求并将结果进行融合。

服务化的好处是系统的职责变得清晰,可以突破单一资源限制等,比如突破数据库连接资源的限制(包括关系型数据库、非关系型数据库);不太友好的地方如服务分化、治理复杂等,比如页面要展示一个商品就需要调用库存、商品、价格、促销等各种服务。

像库存、商品、价格等这些体量(访问量+数据量)非常大的服务将他们拆分为单一系统(一个系统只提供一种服务)是很有必要的。对于体量不够大的或者职责划分不清的服务,为了便于维护和使用,一般会将其融合在一个系统中(暂且称它为”非单一系统”)。这些服务一个共同的特点是读大于写,比如京东首页的全部分类、热搜索词等, 可以说是一个彻彻底底的读服务,这些信息数据量小而且很少改动,读取量远远高于写入(或更新)量,像单品页要用到的延保、pop套装等服务,虽然对于单个商品他们的读写不频繁,但他们会涉及很多(亿级别)sku,所以整体加起来他们的访问量、数据量、更新频率都不小。那么针对这些五花八门的服务,怎么才能在一个系统里,既要保证高可用,又保证高性能,还要保证数据一致性等问题,下面我们就来一一解答。

系统特点

  1. 提供的服务多
  2. 依赖的数据源多样化,数据库、HTTP接口、JSF(公司内部RPC框架)接口等
  3. 系统以读为主
  4. 整体服务加起来体量大(访问量+数据量)
  5. 需要快速响应
  6. 服务之间相互影响性要小

基本原则

根据以上系统特点,我们实现该系统时遵循以下几个大的原则:

  1. 使用HTTP协议对外通信
  2. 使用短连接
  3. 数据异构
  4. 巧用缓存
  5. 流量控制
  6. 异步、并行
  7. 数据托底
  8. 防刷
  9. 降级
  10. 多域名

使用HTTP协议对外通信

前面提到服务化后各个系统使用的语言可以不相同,对于使用同一种语言实现的不同系统,可以指定语言相关的协议进行通信,比如JSF(公司内部RPC框架),不同语言的系统之间就需要找一个通用的协议来通信。SOAP简单对象访问协议是一种非语言相关的通信协议, 以HTTP协议为载体进行传输,虽然有各种辅助框架,但它还是太重了,相比较HTTP从便捷和使用范围上有绝对的优势,所以本系统以HTTP协议对外提供服务。

使用短连接

HTTP协议本身是工作在TCP协议上的,这里说的长连接短连接本质上只的是TCP的长短连接。所谓的长连接顾名思义就是用完之后不立即断开连接,何时断开取决于上层业务设置和底层协议是否发生异常,短连接就比较干脆,干完活马上就将连接关闭,过完河就拆桥。

在HTTP中开启长连接需要在协议头中加上Connection:keep-alive,当然最终是否使用长连接通信是需要双方进行协商的,客户端和服务端只要有一方不同意,则开启失败。长连接因为可以复用链路,所以如果请求频繁,可以减少连接的建立和关闭时间,从而节省资源。

HTTP 1.0默认使用短连接,HTTP 1.1中开启短连接需要在协议头上加上Connection:close,如何单个客户请求频繁,TCP链接的建立和关闭多少会浪费点资源。

既然长连接这么『好』,短连接这么『不好』为什么还要使用短连接呢?我们知道这个『连接』实际上是TCP连接。TCP连接是有一个四元组表示的,如[源ip:源port—目标ip:目标port]。从这个四元组可以看到理论上可以有无数个连接, 但是操作系统能够承受的连接可是有限的,假设我们设置了长连接,那么不管这个时间有多短,在高并发下server端都会产生大量的TCP连接,操作系统维护每个连接不但要消耗内存也会消耗CPU,在高并发下维护过多的活跃连接风险可想而知。

而且在长连接的情况下如果有人搞恶意攻击,创建完连接后什么都不做,势必会对Server产生不小的压力。所以在互联网这种高并发系统中,使用短连接是一个明智的选择。对于服务端因短连接产生的大量的TIME_WAIT状态的连接,可以更改系统的一些内核参数来控制,比如net.ipv4.tcp_max_tw_buckets、net.ipv4.tcp_tw_recycle、net.ipv4.tcp_tw_reuse等参数(注:非专业人士调优内核参数要慎重)。

具体TIME_WAIT等TCP的各种状态这里不再详述,给出一个简单状态转换图供参考:

gaobingfa-01

数据异构

一个大的原则,如果依赖的服务不可靠,那系统就可能随时出问题。对于依赖服务的数据,能异构的就要拿过来,有了数据就可以做任何你想做的事,有了数据,依赖服务再怎么变着花的挂对你的影响也是有限的。

异构时可以将数据打散,将数据原子化,这样在向外提供服务时,可以任意组装拼合。

巧用缓存

应对高并发系统,缓存是必不可少的利器,巧妙的使用缓存会使系统的性能有质的飞跃,下面就介绍一下本系统使用缓存的几种方式。

使用Redis缓存

首先看一下使用Redis缓存的简单数据流向图:

gaobingfa-02很典型的使用缓存的一种方式,这里先重点介绍一下在缓存命中与不命中时都做了哪些事。

当用户发起请求后,首先在Nginx这一层直接从Redis获取数据, 这个过程中Nginx使用lua-resty-redis操作Redis,该模块支持网络Socket和unix domain socket。如果命中缓存,则直接返回客户端。如果没有则回源请求数据,这里要记住另一个原则,不可『随意回源』(为了保护后端应用)。为了解决高并发下缓存失效后引发的雪崩效应,我们使用lua-resty-lock(异步非阻塞锁)来解决这个问题。

很多人一谈到锁就心有忌惮,认为一旦用上锁必然会影响性能,这种想法的不妥的。我们这里使用的lua-resty-lock是一个基于Nginx共享内存(ngx.shared.DICT)的非阻塞锁(基于Nginx的时间事件实现),说它是非阻塞的是因为它不会阻塞Nginx的worker进程,当某个key(请求)获取到该锁后,后续试图对该key再一次获取锁时都会『阻塞』在这里,但不会阻塞其它的key。当第一个获取锁的key将获取到的数据更新到缓存后,后续的key就不会再回源后端应用了,从而可以起到保护后端应用的作用。

下面贴一段从官网弄过来的简化代码,详细使用请移步https://github.com/openresty/lua-resty-lock

gaobingfa-03

使用Nginx共享缓存

上面使用到的Redis缓存,即使Redis部署在本地仍然会有进程间通信、内核态和用户态的数据拷贝,使用Nginx的共享缓存可以将这些动作都省略掉。

Nginx共享缓存是worker共享的,也就是说它是一个全局的缓存,使用Nginx的lua_shared_dict配置指令定义。语法如下:

#指定一个100m的共享缓存
lua_shared_dict cache 100m;

数据流向图如下:

gaobingfa-04

缓存分片

当缓存数据的总量大到一定程度后,单个Redis实例就会成为瓶颈,这时候就要考虑分片了,具体如何分片可以根据自己的系统特性来定,如果不是对性能有苛刻的要求,可以直接使用一些Redis代理(如temproxy),因为代理对Redis性能有一定的损耗。

使用代理的另一个好处是它支持多种分片算法,而且对用户是透明的。我们这里没有选择代理,而是自己实现了一个简单的分片算法。

该分片算法在Java端基于Jedis扩展出一套取摸算法,向Redis写数据。Nginx这端使用lua+c实现同样的算法,从Redis读数据。

另一种是对Nginx的共享缓存(dict)做分片,dict本身使用自旋锁加红黑树实现的,它这个锁是一个阻塞锁。同样当缓存在dict中的数据量和访问并发量大到一定程度后,对其分片也是必须的了。

缓存数据切割

早前阅读Redis代码发现Redis在每个事件循环中,一次最多向某个连接吐64K的数据,也就是说当缓存的数据大于64K时,至少需要两个事件循环才能将数据吐完。当然,在网络发生拥堵或者对端处理数据慢时,即使缓存数据小于64K,也不能保证在一个事件循环内吐完数据。基于这种情况我们可以考虑,当数据大于某个阀值时,将数据切割成多个小块,然后将其放到不同的Redis上。

简单描述下实现方式:

  1. 存储时先判断数据大小(数据大小用n表示,阀值用a表示),如果n大于a则代表需要将数据切割存储,切割的块数用b表示,b是Redis的实例个数,用n整除b得出的数c是要切割的数据块(前b-1块)的大小,最后一块数据的大小是n-c*2。存储前生成一个版本号,将这个版本号放到被切割块的第一个字节,然后按照顺序异步将其存入各个Redis中,最后再为代表该数据的key打上标记,标记该key的数据是被切割的。
  2. 取数据时先检查该key是否被标记,如果被标记则使用ngx.thread.spawn(),按照顺序异步并行向各个Redis发送get命令,然后对比获取到的所有数据块的第一个字节,如果比对一直,则拼装输出。

注:这种算法用在Nginx的共享缓存不会有性能的提升,因为共享缓存的操作都是阻塞操作,只有支持非阻塞操作的网络通信才会对性能有提升。

缓存更新

根据业务的不同,缓存更新的方式也各有不同,一种容易带来隐患的方式是被动更新,这种更新方式在缓存失效后,需要通过回源的方式来更新缓存,这时需要运用多种手段来控制回源量(比如前面说到的非阻塞锁)。

另一种我们称之为主动更新,主动更新一般有消息、worker(定时器)等方式,使用消息方式可以确保数据实时性比较高,worker方式实时性要少差一点。实际项目中使用哪种方式更新缓存,可以从可维护性、安全性、业务性、实时性等方便找一个平衡点以便选择合适的更新方式。

数据一致性

为了保证服务快速响应,我们的Redis都是部署在本地的,这样可以减少网络传输消耗的时间,也可以避免缓存和应用之间网络故障造成的风险。这个单机部署会造成相互之间数据不一致,为了解决这个问题,我们使用了Redis的主从功能,并且Redis以树形结构进行部署,这时每个集群一个主Redis,同一个集群中的服务都向主Redis写数据, 由主Redis将数据逐个同步下去,每个服务器只读自己本机的Redis。

Redis的部署结构像这样:

gaobingfa-05

在描述缓存更新时,提到了worker更新,基于上述的Redis部署方式,我们用worker更新缓存时会存在一定的问题。如果所有的机器都部署了worker,那么当这些worker会在某个时刻同时执行,这显然是不可行的。如果我们每个集群部署一个worker,那么势必造成单点问题。基于以上问题我们实现了一种分布式worker,这种worker基于Redis以集群为单位,在一个时间段内(比如3分钟)只会有一个worker被启动。这样既可以避免worker单点,又可以保持代码的统一。

流量控制

这一原则主要为了避免系统过载,可以采用多种方式达到此目的。流量控制可以在前端做(Nginx),也可以在后端做。

我们知道servelt 2.x在处理请求时用的是多线程同步模型,每一个请求都会创建一个线程,然后同步的执行该请求,这个模型受限于线程资源的限制,很难产生大的吞吐量,而且某个业务阻塞就会引起连锁反应。基于servlet 2.x的容器我们一般采用池化技术和同步并行操作,使用池化技术可以将资源进行配额分配,比如数据库连接池。同步并行需要业务特性的支持,比如一个请求依赖多个后端服务,如果这些后端服务在业务上没有一个先后顺序的依赖,那么我们完全可以将这些服务放到一个线程池中去并行执行,说它同步是因为我们需要等到所有的服务都返回结果后才能继续向下执行。

目前servlet 3支持异步请求,是一种多线程异步模型, 它的每个请求仍然要使用一个线程,只不过可以进行异步操作了。这种模型的一个优点是可以按业务来分配请求资源了,比如你的系统要向外提供10中服务,你可以为每种服务分配一个固定的线程池,这样服务之间可以相互隔离。

缺点是由于是异步,所以就需要各种回调,开发和维护成本高。同事有一个项目用到了servlet 3,测试结果显示这种方式不会获得更短的响应时间,反而会有稍微下降,但是吞吐量确实有提升。所以最终是否使用这种方式,取决于你的系统更倾向于完成哪种特性。

除了在后端进行流量控制,还可以在Nginx层做控制。目前在Nginx层有多种模块可以支持流量控制,如ngx_http_limit_conn_module
、ngx_http_limit_req_module、lua-resty-limit-traffic(需安装lua模块)等,限于篇幅如何使用就不在详述,感兴趣可到官网查看。

数据托底

生产环境中有些服务可能非常重要,需要保证绝对可用,这时如果业务允许,我们就可以为其做个数据托底。

托底方式非常多,这里简单介绍几种,一种是在应用后端进行数据托底,这种方式比较灵活,可以将数据存储在内存、磁盘等各种设备上,当发生异常时可以返回托底数据,缺点是和后端应用高度耦合,一旦应用容器挂掉托底也就不起作用了。

另一种方式将托底功能跟应用剥离出来,可以使用Lua的方式在Nginx做一层拦截,用每次请求回源返回的正确数据来更新托底数据(这个过程可以做各种校验),当服务或应用出问题时可以直接从Nginx层返回数据。

还有一种是使用Nginx的error_page指令,简单配置如下:

gaobingfa-06

降级

降级的意义其实和流量控制的意义差不多,都是为了确保系统负载稳定。当线上流量超过我们预期时,为了降低系统负载就可以实施降级了。

降级的方式可以是自动降级,比如我们对一个依赖服务可以设置一个超时,当超过这个时间时就可以自动的返回一个默认值(前提是业务允许)。

手动降级,提前为某些服务设置降级开关,出现问题是可以将开关打开,比如前面我们说到了有些服务是有托底数据的,当系统过载后我们可以将其降级到直接走托底数据。

防刷

对于一些有规律入参的请求,我们可以用严格检验入参的方式,来规避非法入参穿透缓存的行为(比如一些爬虫程序无限制的猜测商品价格),这种方式可以做在前端(Nginx),也可以做在后端(Tomcat),推荐在Nginx层做。 在Nginx层做入参校验的例子:

gaobingfa-07

使用计数器识别恶意用户,比如在一段时间内为每个用户或IP等记录访问次数,如果在规定的时间内超过规定的次数,则做一些对应策略。

对恶意用户设置黑名单,每次访问都检查是否在黑名单中,存在就直接拒绝。

使用Cookie,如果用户访问是没有带指定的Cookie,或者和规定的Cookie规则不符,则做一些对应策略。

通过访问日志实时计算用户的行为,发现恶意行为后对其做相应的对策。

多域名

除正常域名外,为系统提供其它访问域名。使用CDN域名缩短用户请求链路,使用不带Cookie的域名,降低用户请求流量。

总结

以上大致介绍了开发以读为主系统的一些基本原则,用好这些原则,单台机器每天抗几十亿流量不是问题。另外上面提到的好多原则,限于篇幅并没有详细展开描述,后续有时间再详细展开。

作者介绍

马顺风,目前就职于京东商城,曾参与开发并设计过多个亿级流量系统,擅长解决大并发、大流量问题。作为一个不安分的Java码农,懂点Lua会点C,业余时间喜欢在Redis、Nginx、Nginx+Lua等方面瞎折腾。

Java最佳实践

Java 是在世界各地最流行的编程语言之一, 但是看起来没人喜欢使用它。而 Java 事实上还算是一门不错的语言,随着 Java 8 最近的问世,我决定编制一个库,实践和工具的清单,汇集 Java 的一些最佳实践。

本文被放到了 Github 上。你可以随意地提交贡献,并加入自己的有关 Java 方面的建议和最佳实践。

本文概览:

  • 风格
    •  Javadoc
    • 构建器模式
    • 结构
    •  依赖注入
    • 避免空值
    • 默认不可变更
    • 避免大量的工具类
    •  格式化
    •  流
  • 发布
    •   依赖收敛
    •   框架
    •   Maven
    •   持续集成
    •   Maven 资源库
    •   配置管理
    •   jUnit 4
    •   jMock
    •   AssertJ
    •   Apache Commons
    •   Guava
    •   Gson
    •   Java Tuples
    •   Joda-Time
    •   Lombok
    •   Play framework
    •   SLF4J
    •   jOOQ
    •  Missing Features
    •   Testing
  • 工具
    •   Chronon
    •   IntelliJ IDEA
    •   JRebel
    •   Checker 框架
    •   Eclipse 内存分析器
  •   资源
    • 书籍
    • 播客

风格

通常,我们会以一种非常详细繁杂的企业级 JavaBean 的风格进行 Java 代码的编写。新的风格则更加清晰,正确,且看上去也更加的简单。

结构

作为程序员的我们要做的最简单的事情之一,就是传递数据。一般的方式就是定义一个 JavaBean:

为什么多元化和内涵很重要,怎样驱动它们?

从一个有天分和能力的团队中借助和利用不同的想法、视角和经验,无论他们的组织地位和背景如何,都能在组织里产生效果,这是组织D&I顾问Shaheen Akram。还有几个人则指出里为什么多元化和内涵对于达到商业目标和成为承担社会责任的组织是很重要的。

在他的博客文章《我们为了保持建立一个多元化的编辑操作方式都做了什么》中,Buzzfeed的主编Ben Smith是这样定义多元化的:

一个有足够多的人的组织,无论成员的种族、性别、性取向、宗教、性别认同、社会经济背景或残疾与否,没有一个人必须代表他们组织应该持有的观点。

在2016年QCon New York大会中,BuzzFeed的工程总监Swati Vauthrin探讨了有关多元化的话题,她说对于Buzzfeed来说,招聘的多元化是一个道德约束:

所有对科技感兴趣并且想要努力工作、想要学习、创造和交付产品的个体都应该被给予同样的机会。

她还认为,这还是一种良好的实践,因为寻找素质高的工程师很难。

在一场有关组织多元化的商业案例的采访中,硅谷银行副总裁Regina Chien说:

一个组织中的高层领导会为组织奠定一个基调,通过他们的行动和故事告诉大家为什么多元化对商业目标是很重要的。建立好商业目标并且展示出多元化和内涵是能够成功的策略是不容易的,但是这非常关键。多元化和内涵不能成为独立的项目。

Atlassian的Sven Peters在他的有关代码文化的文章中解释了如果你想要建立一个能够开发出更好产品的文化时多元化的重要性:

你不应该只雇一种类型的员工。相信我所说的,你不需要一堆程序员,并且如果你只雇用你的朋友时这很可能会发生。你的文化和生产率会随着多元化而成长。如果文化没有受到新的影响者鼓舞就会消亡。雇用女性,雇佣不同年龄、不同背景、不同学历、工作经验或种族的人,而且要确保他们是受欢迎的。

Akram会在2016年伦敦激发改变大会上探讨驱动多元化和内涵的话题。InfoQ对她进行了采访,其中涉及了领导在驱动多元化和内涵中扮演的角色、组织可以做什么来创造一个有内涵的环境、多元化和内涵如何互相支持、多元化怎样提升团队的协作能力,以及企业通过拥有支持多元化和内涵的策略能获取什么好处。

InfoQ:领导在驱动多元化和内涵中扮演什么角色?

Shaheen Akram:不管有没有职位名称的领导都有很大的机会为多元化和内涵奠定基调、理念和实践。与商业目标相结合,他们的目标就是通过每天的交流将理论应用到生活中。以我的经验来说,这是最重要的——不是说了什么而是做了什么。这开始于领导理解了变化的外在和内在版图和它们如何影响一个组织来思考、创新和做出改变。会分享有个性的故事的领导,强调他们的“电灯泡”时刻,提供了非常强大的激励机制。

而且,分享真实例子,例如“我想要创造一个地方,我的女儿们可以进入一个她们可以茁壮成长的工作场所,并且我希望我的儿子们能够接触到所有可用的机会”,这显示出我们都是人,也表明了永远在改变的版图需要我们适应它们并做出改变,不仅仅是在商业中存活而是更加繁荣。为了确保组织们在变化,衡量进度是很重要的——我们都听说过“可以衡量的工作才能完成”的俗语。我们应该阶段性地、透明地审核进度,并将其与更广泛的组织分享,而且好的工作和个人应该被认可。

InfoQ:组织可以做什么来创造一个有内涵的环境,其中每个人都被给予同等的机会?

Akram:有很多的实践和例子就在组织们一直尝试建立一个更加有内涵的环境的地方。这没有什么高招,但是以我的经验来说,管理者和员工的关系不仅在建立有内涵的环境时非常关键,在真正感受并齐心协力时也很重要。员工是精明的个体;他们知道什么时候他们被聆听,他们知道如果一个管理者是否真的理解他们,他们知道他们是否可以开诚布公地说话。人们常说,是人离开管理者,而不是离开公司。所以雇佣有效的管理者是很关键的,并且在我的经验中这是起点。鼓励管理者建立更好的关系、信任并扩大他们的员工、意识到潜在的偏见或成见,这样他们就会增加双方的经验。他们应该按照参与度、承诺和会议的目标来传递积极的结果。另外,确保政策、程序和实践是通过“内涵透镜”审核的,这样他们就不会无心地排除员工,这也是一个必须的步骤。

InfoQ:多元化和内涵如何互相支持?

Akram:我经常听到领导说“我们是一个多元的组织”,但是当你环视周围时会发现组织经常由外表相似的人组成,如果用他们的人口统计学背景或者工作经验来统计也是同样的。以我的经验来说,这很大地影响了周围的思想多样化,因此影响了组织的决定。我们暂时暂停一下并思考高层领导的团队…他们有多么不同和有内涵吗?所以我说还有很多工作要做,因此这是一个很好的机会。

一个高效的组织应该努力利用不同的观点、角度,以及有天赋的、有能力的员工带来的经验,不管他们在组织架构中处于什么地位。给组织带来“多元化”然后又排除人们的贡献是毫无意义的——你本可能不需要这么麻烦。目标是让人们充分地展示、贡献并发挥价值。

InfoQ:你能举例说明多元化怎样提升团队的协作能力吗?

Akram:不同的、拥有不同经验的人会自动地带给团队不同的想法和工作方式。这样的动力在想法产生并建立的时候非常实用有效。有的时候,发散思维如果没有用正确的方式管理的话,会产生摩擦、甚至混乱。

在我的思想中有一些需要进行的步骤,来确保多元的团队能够高效并成功地协作:

  • 团队齐心协力并在核心目标和工作方式上有共同想法是很关键的。
  • 很强的对于队伍的管理可能需要来确保所有人都遵守规则。
  • 应该对想法、辩论和讨论、测试想法分不同的场合,并且最终共同达成一致。
  • 应该鼓励打断。在我的经验中,恭敬地挑战想法是很重要的。引发思考很重要,而且它经常能产生强大的团队成果。

InfoQ:企业通过拥有支持多元化和内涵的策略能获取什么好处?

Akram:外面有很多研究展示了多元化和内涵在经济和非经济方面的力量。全世界的大型和小型组织都积极地利用这种力量。对于组织来说,问题在于他们是否想要服从、竞争或领导这个空间?

在人口变化、激烈的竞争、新生的市场、客户变化的需求和有天赋的人的愿望中,如果组织想要茁壮成长的话,他们必须找到方法来更好地连接客户、人们和社区。创新、协作和工作伙伴关系都是重要的因素。为了让客户和员工获得最好的产品和服务,组织需要工作并利用差异,用不同的方式做事并承担可以估计的风险。这样做的话,我相信,就会驱使组织增加他们的盈亏线、为客户和员工都传递更好的结果,并且会被看作是他们运作的社区中负责的商业。胜利,胜利,再胜利!

2016年伦敦激发改变大会会在7月6-7号举行。这次大会带来了各个商业的领导们并探索他们如何共同工作来创造持久的总改变。它的目标是鼓舞人们并实际地帮助人们跨越障碍并开发潜力来做出改变。InfoQ会在伦敦激发改变大会上持续跟拍会议并通过Q&A、总结和文章来报导会议情况。

查看英文原文Why Diversity and Inclusion Matters, and How to Drive It

Basho公司开源了它的时序数据库产品Riak TS 1.3

Basho科技公司发布了Riak TS 1.3的开源版本。Riak TS是专门面向时序数据处理的产品。它支持时序数据的快速写入和查询。此外,Riak TS的特性还包括:支持数据聚集和算术运算操作,通过Spark连接器与Apache Spark的集成,对Java、Erlang和Python等语言的客户端支持,基于标准SQL的查询系统。Riak TS 1.3 EE(企业版)是基于支持多集群复制的开源版本而构建。在它的发行说明中,给出了其所有特性的完整列表。

针对本次发布,InfoQ择机访谈了Basho公司的CTO,Dave McCrory先生。

InfoQ:您能对Riak产品套装,尤其是Riak TS产品,做一个概要介绍吗?套装产品的开发将依然延续彼此独立的方式吗?

McCrory: Riak产品家族都是围绕着Riak内核构建的,Riak内核是过去七年中Basho公司一直致力于开发的开源集群解决方案。

Riak KV产品构建于Riak内核之上,提供了一种高弹性、高可用的键值数据库。Riak KV产品当前正在持续改进中,专注于数据正确性、预防数据损失和破坏等特性。

Riak TS产品源于Riak KV数据库,是一种为时序数据仓库而专门构建的产品。其中集成了Riak KV产品的所有强大功能,并使用这些功能去解决用户在处理时序数据中所遇到的问题。我们在该产品中确实地实现了哪些特性呢?这里我列出了其中的一部分:

  • 数据的快速写入路径;
  • 为数据桶建立模式;
  • 查询规划及查询子系统;
  • 对虚拟节点的并行数据抽取;
  • 灵活的复合键值;

InfoQ:Riak TS产品的开发已持续近18个月了,你能介绍一下这个开发经历吗?

McCrory: 在早期,我们看到客户用Riak KV产品去保存时序数据。当我们审视其中的需求时,发现为使这样的系统顺利运行,需要做很多的产品定制工作。我们也查看了时序数据库产品的市场情况,当时只见到了寥寥可数的几个解决方案,并且所有这些解决方案的质量都不足以承担企业级的生产工作负荷。已有的时序数据解决方案或者是缺乏可扩展集群或弹性,或者是管理和操作非常繁琐。所有这些使得它们成为糟糕的选择。

为讨论解决这个问题的创意,我们进而开了一次架构会议。最终,我们的一个工程师提出了一个有意思的创意,即使用量子(时间范围)将数据围绕哈希环分布,并基于此创意构建了一个看上去运行良好的概念验证原型。依此我们开始了Riak TS产品的开发过程,力图去解决许多时序数据处理中更加困难的问题。

开发过程中还有其它一些限制需要处理,包括对性能的需求。譬如要在八台机器组成的集群中支持每秒百万级别数据点的性能。为实现该性能需求,需要对从涌入集群中直至磁盘存储过程中的数据流进行描述分析,并返回数据读取的路径信息。当我们开始注意这个问题时,我们就认识到需要有一个专注于性能问题的专门团队。这样,我们就成立了性能团队。

性能团队力图找出项目中效率低下的部分,这些部分很大程度上是在项目早期的开发中产生的,它们是紧随着对如何做端到端程序概要分析的学习过程。这样可给出一组问题,重点去解决这些问题就能为性能提高的工作带来最大的回报。其中我们所发现的一个重要问题是,我们正在做多重的解码编码,这消耗了CPU周期,并增加了许多延迟,这样我们就开始去消除这些问题。

同时我们也寻求在无需依赖于二级索引的条件下,迅速响应各种时序数据查询的方法。因为时序数据具有内在的结构,我们于是着眼于对数据构建模式。该做法也为我们实现查询引擎和查询语言等开启了大门。最终我们决定使用SQL查询语言,而非像其它供应商那样选择自定义的SQL,因为我们发现那样会使得事情更加复杂。相比较于从所有包含数据的节点或从整个集群中所有的节点请求数据的方法,我们仅从拥有指定数据的节点集中请求数据。这种实现必须去重构查询解析器和查询规划器,这种做法被证实是具有挑战性的。但是该方法所给出的弹性和性能指标说明了一切。

InfoQ:除了在物联网中的使用案例之外,还有哪些使用Tiak TS产品的好的案例?

McCrory: 一些顾客和潜在客户已全面使用该产品去解决与时间相关的数据问题。这些问题包括信息报告和审计、分数和下注记录、度量信息的存储等。几乎每个星期我都能听到一些Riak TS产品的新应用。

InfoQ:Basho公司是Riak TS产品的最大贡献者吗?还有哪些公司对此也有贡献?代码开源的目标是什么?

McCrory: Basho公司当然是Riak TS的首要贡献者,主要原因在于为了实现时序解决方案的构建,必须由我们自己去实现许多增改。我们当前正就如何一起构建Riak TS的一系列功能的问题与一些公司会谈,以解决在时序数据处理领域中一些更广泛的问题。

对于代码开源的目标,我们相信对于分布式系统中的创新性方法、创意和领导力等问题,我们有许多可以贡献给开源社区的东西,我们也希望能与更好的解决方案合作。当将借助于开源代码作为产品实现路径时,几乎总是会加速你的实现过程的。我们在自家软件的开源上具有很悠久的历史,也在创造更好的解决方案过程中得到了社区的支持。

InfoQ:Apache Spark连接器是使用Riak TS解决大数据问题中的一个重要组件,我说的没错吧?你能介绍一下这个连接器,以及它与其它Apache Spark的NoSQL连接器相比究竟如何?

McCrory: 自Apache Spark进入大数据分析行业以来,在我们所接触的大多数客户中,它都当然是行业翘首。当然我们的使用Riak TS的大数据解决方案的客户中,也有一些并没有使用Apache Spark的案例。这些案例都是不需要复杂分析、集群学习及其它一些Apache Spark所提供的特性的。这意味着,我们的大数据客户中的大多数是使用具有Spark连接器的Riak TS和Apache Spark的。

我们开发Riak的Spark连接器已经有一段时间了,当前该功能的实现已经历了至少三轮测试。我们所做的事情之一是,允许Spark连接器使用我们所开发的并行抽取特性。通过这个实现方法,我们具有了同时从所有节点并行地拉取所有的请求数据的能力。这加速了数据到Spark的加载,并且当借助于Apache Spark和Spark连接器时,通过我们所实现的写性能提高功能,使得Riak TS成为的一个优秀的解决方案。

InfoQ:你能提供包括HTTP API安全支持和聚焦于物联网在内的Riak TS产品的路线图吗?

McCrory: 我们期望在即将推出的Riak TS 1.4版中具有HTTP API安全等一些特性,其中包括:更全面的SQL命令支持、初步的结果流支持及其它一些功能。

使Riak TS聚焦于物联网的核心问题,是我们如何提供非常易于操作和使用的时序数据库。通过聚焦于物联网数据,我们看到了对于持续小规模数据块的越来越高的数据通量的需求。我们将继续性能优化,并与其它互补的技术对接。当前我们正在探寻的领域是数据可视化,即我们如何能使Riak TS与在开源领域和工业界十分流行的可视化解决方案合作或一起工作。通常,采集和存储所有这些物联网数据的关键在于如何去识别所需采取的动作。

入门指南材料可以在Basho文档中获取。

查看英文原文: Basho Open Sources Time Series Database Riak TS 1.3