分类目录归档:未分类

Mac給树莓派烧录img镜像

Mac給树莓派烧录img镜像

Raspbian是树莓派官方支持的操作系统, 在Mac上给树莓派装该系统很简单。

下载文件

这里下载img文件,如果你需要桌面版,下载Desktop版本,我是不习惯图形界面,就只下载lite版本就好了,节省你的tf卡空间

格式化SD Card

要求TF卡必须是FAT,先用磁盘工具格式化:

名字随便填了,格式选择 MS-DOS(FAT)

千万把磁盘选对了!!切记,点抹掉之前一定在好好看一眼,我曾经曾经格式化过mac系统盘….

烧录img

后面就不用这么紧张了,几步操作完事儿。

终端输入 diskutil list,找到你刚才格式化的TF卡,我的是disk2,我这个卡已经是烧录过img了,所以分区已经有了,如果还没有烧录的,应该是一个分区,有你刚才格式化时候输入的名字

取消挂载

diskutil unmountdisk /dev/disk2

这一步应该是没有输出的

开始烧录img
使用下面的命令写入镜像。注意if参数的值是img文件的path,of参数的值是上一步找到的SD Card的标识符,但是disk2换成了rdisk2。

sudo dd bs=1m if=这个地方环城你img的路径 of=/dev/rdisk2

只需等待⌛️几分钟。完毕后tf卡插回树莓派,开机,第一次开机需要连接显示器,键盘

用户名:pi
密码:raspberry

done!

树莓派3 静态ip地址配置

刚开始玩树莓派,安装系统之类的基本没有问题,就单单这修改ip地址把我卡住了,网上随便找了个贴一步步照着来了一遍,然后我的ssh就连不上了,无奈家中还没有有线键盘,只好把派又带回了公司配置,现在大部分网上的静态ip配置基本都是错的各位玩家慎用慎用

使用ssh登陆,修改/etc/dhcpcd.conf配置文件,来修改网络为静态ip地址

vim /etc/dhcpcd.conf

在dhcpcd.conf文件最后添加如下内容:

interface eth0
static ip_address=192.168.31.254/24
static routers=192.168.31.1

:wq 保存退出

如果你是pi 3B 连接wifi

配置是这样的

interface wlan0
static ip_address=192.168.31.253/24
static routers=192.168.31.1

静态地址配置pi

重启你的pi,reboot


提示:提示:提示:千万不能修改 /etc/network/interfaces,改了你会跟我一样,插键盘,连显示器,从新调试,非常麻烦


广而告之:vultr jp节点搭上梯子1080无压力

scrapy:TypeError: ‘float’ object is not iterable

使用scrapy进行网页爬取时,出现:TypeError: ‘float’ object is not iterable错误,竟然是由于Twisted版本不兼容导致的。具体可参考这里:https://github.com/scrapy/scrapy/issues/2461

修复方法,将Twisted的版本降级到Twisted-16.6.0版本或者小于16.6.0的版本即可。

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等方面瞎折腾。