0%

Elasticsearch

基本概念

You know, for search (and analysis)

基于Lucene的分布式全文搜索引擎

  • 分布式文件存储
  • 分布式(准)实时搜索引擎,每个字段都被索引并可被搜索
  • 分布式(准)实时分析工具,聚合功能可以构建复杂数据摘要
  • 开箱即用。安装好ElasticSearch后,所有参数的默认值都自动进行了比较合理的设置,基本不需要额外的调整。包括内置的发现机制(比如Field类型的自动匹配)和自动化参数配置。
  • 天生集群。ElasticSearch默认工作在集群模式下。节点都将视为集群的一部分,而且在启动的过程中自动连接到集群中。
  • 自动容错。ElasticSearch通过P2P网络进行通信,这种工作方式消除了单点故障。节点自动连接到集群中的其它机器,自动进行数据交换及以节点之间相互监控。索引分片
  • 扩展性强。无论是处理能力和数据容量上都可以通过一种简单的方式实现扩展,即增添新的节点。
  • 近实时搜索和版本控制。由于ElasticSearch天生支持分布式,所以延迟和不同节点上数据的短暂性不一致无可避免。ElasticSearch通过版本控制(versioning)的机制尽量减少问题的出现。

分布式

  • 节点(Node):一个运行的Elasticearch实例。分为主节点、数据节点、协调节点、部落节点
  • 索引(Index),逻辑概念,包括配置信息mapping和倒排正排数据文件
  • 分片(Shard):分为主分片和副本分片。索引按某个维度分成多个部分。一个节点(Node)一般会管理来自多个索引的多个分片,同一个索引的分片尽量会分布在不同节点(Node)上。
  • 副本(Replica):同一个分片(Shard)的备份数据,分片有>=0个副本。

文件存储

  • Index/索引:Index是一类文档的集合,是具有相同业务特征的数据文档集合(不是相同数据结构),相当于传统数据库中的数据库。ES数据的索引、搜索和分析都是基于索引完成的。Cluster中可以创建任意个Index。每个Index(对应Database)包含多个Shard,默认是5个,分散在不同的Node上,但不会存在两个相同的Shard(相同的主和复制)存在一个Node上。当索引创建完成的时候,主分片的数量就固定了,但是复制分片的数量可以随时调整。
  • Type/类型:Type是Index中数据的 ,用于标识不同的文档字段信息的集合,相当于传统数据库的表。在0之后的版本直接做了插入检查,禁止一个索引下不同Type的字段类型冲突。举例来说,在一个博客系统中,你可以定义一个 user type,可以定义一个 blog type,还可以定义一个 comment type。
  • Document/文档:Document是ES数据可被索引化的基本的存储单元,需要存储在Type中,相当于传统数据库的行记录,使用json来表示。

c021d8ded5c9cf4227a5c699e9700f49.png

搜索

  • 结构化查询
  • 全文搜索
  • 嵌套搜索
  • 模糊匹配
  • 相关度查询

分析

  • 时序分析
  • 聚合日志
  • 数据的可视化
  • 数据图谱分析
  • 地理位置分析

原理

分布式

主节点作用:

  • 主节点上有个单独的进程处理 ClusterState(保存集群的状态,索引/分片的路由表,节点列表,元数据等信息的数据结构)的变更操作,每次变更会更新版本号。变更后会通过PRC接口同步到其他节点。主节知道其他节点的ClusterState 的当前版本,发送变更的时候会做diff,实现增量更新。
  • 创建或删除索引
  • 跟踪哪些节点是群集的一部分,并决定哪些分片分配给相关的节点

容灾实现:

集群状态:

  • 绿色——最健康的状态,代表所有的主分片和副本分片都可用;
  • 黄色——所有的主分片可用,但是部分副本分片不可用;
  • 红色——部分主分片不可用。

Elasticsearch的恢复流程大致如下:

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

选主:

什么时候选主:

  1. 集群启动
  2. Master 失效

为什么不用zk/etcd:

es 集群相对较小,而且拓扑结构相对不容易变动。简单场景简单做。

选举原理:

Bully是Leader选举的基本算法之一。 它假定所有节点都有一个惟一的ID,该ID对节点进行排序。 任何时候的当前Leader都是参与集群的最高id节点。 该算法的优点是易于实现,但是,当拥有最大 id 的节点处于不稳定状态的场景下会有问题,例如 Master 负载过重而假死,集群拥有第二大id 的节点被选为 新主,这时原来的 Master 恢复,再次被选为新主,然后又假死

过程:

  1. 各个节点广播消息,获取集群信息,找出候选节点
  2. 找到id最小的候选节点,推举为临时master节点
  3. 选举
    1. 自认为是临时master的节点开始收集投票
    2. 自认为不是临时master的节点投票给它认为是临时master的节点临时
  4. master收集到一定票数时(为防止脑裂,票数阈值一般是 候选节点总数/2+1)变成主节点,开始构建集群

节点间transport层结构:

每个node间维护13个连接,分别负责恢复数据、传输批量请求、传输正常请求、传输状态、ping

290ed47b83070ef1173fb48b0fa59dd7.png

分片:

Elasticsearch 的分片默认是基于id 哈希的。

总分片数是不能变动的。副本分片数可以变化。分片切分成本和重新索引的成本差不多,所以建议干脆通过接口重新索引。

每一个分片都是一个单独的Lucene索引。

3c638e636665c46cbc4895e821308bc2.png

副本分片和主分片不会放在同一个节点内。

每个节点都有能力处理任意请求。每个节点都知道任意文档所在的节点,所以也可以将请求转发到需要的节点。举例:索引一个新文档

f5b349d660c60b7bdc9b8ccc6a3fee8b.png

Lucene索引

ea35056f56c0f16a442db0140d179da7.png

  1. 多线程并发调用 IndexWriter 的写接口,在 IndexWriter 内部具体请求会由 DocumentsWriter 来执行。DocumentsWriter 内部在处理请求之前,会先根据当前执行操作的 Thread 来分配 DocumentsWriterPerThread。(加锁)
  2. 每个线程在其独立的 DocumentsWriterPerThread 空间内部进行数据处理,包括分词、相关性计算、索引构建等。
  3. 数据处理完毕后,在 DocumentsWriter 层面执行一些后续动作,例如触发 FlushPolicy 的判定等。(加锁)

分段存储:

每个索引被分成了多个段(Segment,由域信息(Field information)、词信息(Term information)、以及其它信息(标准化因子、删除文档)组成),段具有一次写入,多次读取的特点。只要形成了,段就无法被修改。例如:被删除文档的信息被存储到一个单独的文件,但是其它的段文件并没有被修改。

索引过程:

[18,20][女,男]叫term

[1,3][2] 叫posting list

988c1333e452996556b759344e0ebb90.png

过程:

  1. Lucene用户指定好的analyzer解析用户添加的Document。当然Document中不同的Field可以指定不同的analyzer。
  2. term索引
    1. 字符关键词检索
      1. term index:树形结构,通过记录term dictionary的某个前缀的offset,然后从这个位置再往后顺序查找。
        cfec4292c2e5537eecc1b1847ab04018.png
        构建Term Dictionary
      2. FST不但能共享前缀还能共享后缀。不但能判断查找的key是否存在,还能给出响应的输入output。 它在时间复杂度和空间复杂度上都做了最大程度的优化,使得Lucene能够将Term Dictionary完全加载到内存,快速的定位Term找到响应的output(posting倒排列表)
        %!(EXTRA markdown.ResourceType=, string=, string=)
        利用FST(Finite State Transducer,一种类似tire树的有限状态机)压缩term dictionary到内存
    2. 所以为了支持高效的数值类或者多维度查询,lucene 引入类 BKDTree。BKDTree 是K-D树和 B+ Tree树的结合体,对数据进行按照维度划分建立一棵二叉树确保树两边节点数目平衡。在一维的场景下(一维(如整型字段)、二维(如地理坐标类型字段)),KDTree 就会退化成一个二叉搜索树,在二叉搜索树中如果我们想查找一个区间,logN 的复杂度就会访问到叶子结点得到对应的倒排链。如下图所示:
      %!(EXTRA markdown.ResourceType=, string=, string=)
      优点:可以局部更新
      数值关键词
  3. Elasticsearch要求posting list是有序的,有序的posting list可以大大提高搜索速度(特别是面对稀疏索引的场景的时候)。
    es利用Frame Of Reference/Roaring bitmaps 压缩posting list,使得其占用更小的空间,同时还可以满足使用使用跳表进行联合索引过滤
    posting list索引
    1. 使用(id/65535,id%65535)的格式存储数据
      ef21645db82253aa68c9901274cf8491.png
      roaring bitmaps
    2. 增量编码压缩,将大数变小数,按字节存储
      %!(EXTRA markdown.ResourceType=, string=, string=)
      Frame Of Reference

Lucene搜索

segment段内搜索

  1. 用户的输入查询语句将被选定的查询解析器(query parser)所解析,生成多个Query对象。当然用户也可以选择不解析查询语句,使查询语句保留原始的状态。在ElasticSearch中,有的Query对象会被解析(analyzed),有的不会,比如:前缀查询(prefix query)就不会被解析,精确匹配查询(match query)就会被解析。
  2. term搜索(关键词搜索)
  3. 为了能够快速查找 docid,lucene 采用了 SkipList 这一数据结构。SkipList 有以下几个特征:
    posting list搜索
    1. 元素排序的,对应到我们的倒排链,lucene 是按照 docid 进行排序,从小到大。
    2. 跳跃有一个固定的间隔,这个是需要建立 SkipList 的时候指定好,例如下图以间隔是 3
    3. 3eeb8ac70977606c166b3365ffbe3922.png
      SkipList 的层次,这个是指整个 SkipList 有几层

倒排合并

5a4bcbc4824a5df6d0e895b9fc6aa03f.png

在 lucene 中会采用下列顺序进行合并:

  1. 在 termA 开始遍历,得到第一个元素 docId=1
  2. Set currentDocId=1
  3. 在 termB 中 search(currentDocId) = 1 (返回大于等于 currentDocId 的一个 doc),
    1. 因为 currentDocId ==1,继续
    2. 如果 currentDocId 和返回的不相等,执行 2,然后继续

打分

默认是TD-IDF

ES支持重打分。不详细说了

准实时

translog

提供了实时的数据读取能力以及完备的数据持久化能力(在服务器异常挂掉的情况下依然不会丢数据)。Lucene 因为有 IndexWriter buffer, 如果进程异常挂掉,buffer中的数据是会丢失的。所以 Elasticsearch 通过translog来确保不丢数据。同时通过id直接读取文档的时候,Elasticsearch 会先尝试从translog中读取,之后才从索引中读取。也就是说,即便是buffer中的数据尚未刷新到索引,依然能提供实时的数据读取能力。Elasticsearch 的translog 默认是每次写请求完成后统一fsync一次,同时有个定时任务检测(默认5秒钟一次)。如果业务场景需要更大的写吞吐量,可以调整translog相关的配置进行优化。

%!(EXTRA markdown.ResourceType=, string=, string=)

refresh

  • 所有在内存缓冲区中的文档被写入到一个新的segment中,但是没有调用fsync,因此内存中的数据可能丢失
  • segment被打开使得里面的文档能够被搜索到
  • 清空内存缓冲区

%!(EXTRA markdown.ResourceType=, string=, string=)

flush

  • 把所有在内存缓冲区中的文档写入到一个新的segment中
  • 清空内存缓冲区
  • 往磁盘里写入commit point信息
  • 文件系统的page cache(segments) fsync到磁盘
  • %!(EXTRA markdown.ResourceType=, string=, string=)
    删除旧的translog文件,因此此时内存中的segments已经写入到磁盘中,就不需要translog来保障数据安全了

使用场景

常用场景

  • 记录和日志分析
  • 事件数据和指标
  • 数据可视化
  • 全文搜索

其他资料

mongodb vs es

solr vs es

基础

java hashmap

并发编程

volatile关键字

concurrenthashmap

框架

dubbo面试题

动态代理

gdk

利用拦截器(拦截器必须实现InvocationHanlder)加上反射机制生成一个实现代理接口的匿名类,

在调用具体方法前调用InvokeHandler来处理。

JDK动态代理主要涉及两个类:java.lang.reflect.Proxy 和 java.lang.reflect.InvocationHandler

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class Main {
public static void main(String[] args) {
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method);
if (method.getName().equals("morning")) {
System.out.println("Good morning, " + args[0]);
}
return null;
}
};
Hello hello = (Hello) Proxy.newProxyInstance(
Hello.class.getClassLoader(), // 传入ClassLoader
new Class[] { Hello.class }, // 传入要实现的接口
handler); // 传入处理调用方法的InvocationHandler
hello.morning("Bob");
}
}

interface Hello {
void morning(String name);
}

输出:

public abstract void Hello.morning(java.lang.String)

Good morning, Bob

流程:

  1. 定义一个InvocationHandler实例,它负责实现接口的方法调用;
  2. 通过Proxy.newProxyInstance()创建interface实例,它需要3个参数:
    1. 使用的ClassLoader,通常就是接口类的ClassLoader;
    2. 需要实现的接口数组,至少需要传入一个接口进去;
    3. 用来处理接口方法调用的InvocationHandler实例。
  3. 将返回的Object强制转型为接口。

cglib

利用ASM开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。

何时使用JDK还是CGLIB?

  1. 如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP。
  2. 如果目标对象实现了接口,可以强制使用CGLIB实现AOP。
  3. 如果目标对象没有实现了接口,必须采用CGLIB库,Spring会自动在JDK动态代理和CGLIB之间转换。

JDK动态代理和CGLIB字节码生成的区别?

  1. JDK动态代理只能对实现了接口的类生成代理,而不能针对类。
  2. CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,
    并覆盖其中方法实现增强,但是因为采用的是继承,所以该类或方法最好不要声明成final,
    对于final类或方法,是无法继承的。

Spring如何选择用JDK还是CGLIB?

  1. 当Bean实现接口时,Spring就会用JDK的动态代理。
  2. 当Bean没有实现接口时,Spring使用CGlib是实现。
  3. 可以强制使用CGlib(在spring配置中加入<aop:aspectj-autoproxy proxy-target-/>)。

AKF扩展立方

29876be429c5ec4c0fbd37eb08785aa8.png

X轴扩展

在负载均衡之后运行应用的多个拷贝。这是最简单最常用的扩展方式。

缺陷:

  • 每个拷贝需要访问所有的数据,对缓存机制要求很高,数据库很可能成为瓶颈。
  • 不会减少日益增长的开发复杂度。

Y轴扩展

把整个应用切分为不同的服务,每个服务负责一个或少量几个关系相近的函数。

有好几种解耦拆分方式。一种是按照动词分割,一种是按照名词分割。

以购物网站为例:按照动词分割就是按照操作分割,服务1只负责购买流程,服务2只负责售后流程,服务3只负责广告投放流程;

按照名词分割就是按照对象类型分割,服务1只负责商品信息;服务2只负责用户信息。

两种扩展经常是同时使用的!

微服务化就是Y轴扩展的一个重要方式。

Z轴扩展

Z轴扩展和X轴扩展很像,但是在Z轴扩展中,每个服务只跑特定的一部分代码或数据集。

Z轴扩展一般用来扩展数据库。(数据库分片)

在做某些查询的时候,查询指令被送给每一个分片,然后分别查询,最终获得的结果聚合之后返回。

做写入的时候,只需要按照分片键,找到对应的实例进行写入即可。

Z轴扩展的优势:

  • 每个服务器只处理一部分数据
  • 优化了缓存的利用率,减少内存使用和I/O
  • 增强了事务的扩展性
  • 故障隔离

劣势:

  • 增加了整体的复杂度。
  • 需要实现分片机制。万一有重新分片的需求的话,令人头大。
  • 增加复杂度的同事,并没有降低开发的复杂度,这个需要Y轴扩展配合解决。

SOA

英文全称:service-oriented architecture,这不是一种特定的技术,而是一种分布式计算的软件设计方法。它是一个组件模型,它将应用程序的不同服务通过这些服务之间定义好的接口联系起来。接口是采用中立的方式进行定义的,通用性高,可以独立在不同的硬件平台、操作系统和编程语言上进行使用。这使得构建在各种各样的系统中的服务可以以一种统一和通用的方式进行交互。(引用维基百科)

以下指导原则是开发、维护和使用SOA的基本原则:

  1. 可重复使用,高交互操作性。
  2. 通用的匹配开放标准,对各厂商的产品兼容性高。
  3. 服务的识别和分类,提供和发布,监控和跟踪

d6dac2b12b7ee4e87ead3ae656a4a21d.png

传统微服务(Micro-service)框架

1b1ebe294f145e671824b8d9cea58870.png

问题

  1. 语言碎片化
    字节跳动的在线服务编程语言比较分散,除了常见的Go和Python之外,还有Node.js, C++, Java, Rust, R等语言。
    我们知道,要实现一个完善的微服务框架,需要有服务发现、负载均衡、超时控制、熔断、限流、访问控制、并发控制、流量调度等复杂的功能。
    在微服务框架体系下,这些功能需要使用每种语言实现一遍,这将会导致巨大的开发成本和维护成本。
    而由于语言本身的差异,各种服务治理规则无法完全对齐。
    譬如:
  • 以Python的进程模型,很难提供并发数限制的能力
  • C++的流量分流策略与其他语言不一致
  1. 更新困难
    推动用户升级框架往往是一件非常困难的事情,用户有可能还会持续使用你一年一前发布的框架代码。
    当某个框架版本发现重大Bug的时候,要推动相关服务升级到修复后的版本是一件非常困难的事情。
    当需要推动一个全链路新功能的时候,譬如自定义分流,我必须推动整个链路的上下游全部升级,才能最终把整个功能上线。
    假设推动了一半业务升级后,又发现了一个严重的Bug,那将是一场噩梦!
  2. 协议多样化,难兼容
    对于RPC来说,Thrift的不同版本之间是不兼容的,Thrift和FBThrift有一定的差异;
    线上的流量是HTTP, Thrift, gRPC, MySQL等协议并存。
    流量的协议不一样,但很多流量管理的需求却是类似的,如超时配置、跨机房流量调度等,开发者很想对不同的协议施加统一的管理能力。
    当然,以协议本身的复杂混乱程度,仅靠服务化框架是很难统一管理的。
  3. 服务治理消耗巨大
    以Python为例,Python是一种解释性语言,它的性能是向来被人诟病的。上述超时控制、熔断、限流等一系列服务治理规则用python实现一遍,资源消耗非常高。
    一般地,python服务中,用于处理各种服务治理部分的CPU消耗,占总CPU消耗的50%以上。
  4. 业务侵入性强
    框架向业务程序注入了很多与业务无关的代码。
    业务开发者在分析自己的服务的时候,可能会发现有很多服务治理相关的并发线程,对于业务的研发和性能优化有很多干扰。

头条service mesh

7dc9000dcde79d8a5ba3c0ccdbaaee4b.png

背景&目的

传统微服务架构问题

  1. 各语言框架, 服务治理策略各自开发,导致重复开发、策略不一致问题
  2. 已有框架的迭代更新受限于业务的配合程度,迭代更新困难
  3. 服务治理策略迭代和新功能支持受限于架构和更新速度
  4. 不支持多语言、多协议,无框架语言无法使用架构组提供的基础服务
  5. MySQL, 81nginx等SaaS服务希望使用微服务框架的部分能力

目的

  1. 服务框架与业务解耦,框架的升级完全自主可控,不再依赖业务方
  2. 多协议支持且可扩展,最大化框架的覆盖度
  3. 统一、中心化的服务发现和治理方案,与语言和框架无关
  4. 业务可灵活自定义服务发现、服务治理规则并快速应用到线上
  5. 统一的监控、告警、日志和tracing解决方案,为智能运维系统提供支持
  6. 与现有服务框架的兼容、平滑升级
  7. 支持新的RPC框架和SaaS(MySQL, Redis, HTTP)等快速、低成本接入和平滑迁移
  8. 支持线上、线下等多套环境,方便服务的开发和线上测试

市面上mesh分析

  1. Istio架构分析
  2. 蚂蚁金服SOFA Service Mesh分析
  3. 京东Service Mesh分析
  4. ucloud的Service Mesh分析

架构&设计

总体结构

5c8ebc5b02bfb40fce6b2b8406eace43.png

  1. Client SDK
    提供给Service Mesh的使用方,以SDK的方式集成于业务代码或直接作为工具使用,来获得Service Mesh平台的服务能力。与用户业务紧耦合。
  2. Control Panel
    以Web服务的方式提供给使用方,供业务方对基于Mesh的服务进行服务治理操作。通过统一抽象和灵活定制,与业务方的业务逻辑松耦合。
  3. Monitor Panel
    基于与公司现有监控、告警服务的整合,提供在Service Mesh场景下的服务监控、日志、tracing等能力
  4. Data Panel
    请求代理转发,应用Control Panel的服务治理规则,为Monitor Panel提供数据

数据流

service_mesh_数据流.png

头条service mesh系统整体架构

877d8406e2887ee6acd6749663c7a038.png

  1. Mesh-Proxy
    1. 提供流量代理转发功能
    2. 支持多协议,协议插件化
    3. 通过过滤器, 实现服务治理策略
    4. 支持监控统计、tracing、ACL
  2. Control Panel
    1. 将来自MS服务治理平台的控制数据转换为Mesh-Proxy的规则数据并存储至ETCD
    2. 定期备份ETCD/Consul中的相关数据,用于对ETCD/Consul的fallback
    3. 定期与其它机房的Data-Control Server进行数据同步, 应对跨地区请求
    4. 为MS platform的服务治理提供数据源
  3. MS platform
    1. Service Mesh服务信息管理界面
    2. 提供熔断、限流、降级等服务治理配置界面
    3. 提供tracing界面
    4. 提供日志检索界面
  4. 元信息服务
    1. 提供一个Service Mesh服务部署所需要的所有信息: 基础配置、镜像配置、k8s POD配置等
    2. 提供服务的配置信息,包括服务所使用的框架、协议、部署地区、环境类型等, 用于流量调度和服务升级状态查询

mesh proxy

8788d3aa915c5e64b05857a2d358696d.png

  1. Input Transcoder
    协议分为Transport和protocol, 此处仅关注Transport层协议
    请求协议转换模块, 提供对不同请求协议的支持。只要这些协议能提供用于流量调度的基础信息及用于服务治理、智能运维(日志/监控)相关的可选信息,均可以作为Mesh-Proxy的输入请求。
    当前支持HTTP1.1/2、raw Thrift(Pie/Kite框架使用)、raw TCP(MySQL、Redis等使用),也可支持扩展协议。
  2. Input Queue
    输入请求队列,用于抽象不同协议经转换后的消息。消息的结构为: 路由信息+服务治理信息+日志/监控信息+业务数据。
    通过请求队列,支持Mesh-proxy作为ingress proxy时对后端服务的过载保护
    当请求队列满时,可以反馈给主调方减小调用流量
  3. Route
    路由模块, 基于请求中的路由信息,与服务发现模块交互,查找备选的目标cluster列表和endpoint列表。
    对于ingress proxy, 直接将请求路由至本地后端服务。
  4. Service Govern
    根据route中的目标cluster列表和endpoint列表,结合服务治理策略、负载均衡策略选择本次调用的目标地址。将请求添加目标地址后放入Output Queue
  5. Output Queue
    输出请求队列,用于缓冲待发送的请求
    与目标服务的Mesh-Proxy的Input Queue配合完成过载保护
  6. Output Transcoder
    根据待发送后端的Transport协议,将Output Queue中的消息转换为目标协议并发送
    对于新协议,通过插件的方法提供与现有协议的转换函数来完成适配

服务发现与治理

f500f6271b0307a1bf8e0d5afb61138c.png

  1. Mesh Proxy
    1. 在proxy在服务发现和服务治理模块中,通过UDI(uni-data interface, 统一数据接口), 获取所需要的服务cluster信息、endpoint信息、load balance信息、service govern信息等
    2. UDI对外使用gRPC协议进行通信,并通过ETCD、Metrics、Consul、Log、MySQL-Auth等中间件与实际的后端进行通信
    3. UDI内部对不同类型的信息有内存缓存, 用于减少查询次数
    4. UDI支持在ETCD/Consul down掉时,自动切换至Data-Control Server进行查询
    5. 对于非本机房的查询请求,UDI直接请求Data-Control Server进行查询
  2. Service Discovery & Govern Config
    1. 服务发现、流量调度、负载均衡、服务治理相关信息由Control Panel提供。详细的接口和策略见”服务治理问题梳理和优化方案”
    2. Metrics和LogAgent用于收集日志和监控信息
  3. Control Panel
    1. 根据请求和调用方的信息,结合服务治理规则,给出被调的endpoints集合及相关信息,由mesh-proxy去应用
    2. 接收来自服务治理平台(MS)的服务治理规则并存储至ETCD或数据库(MySQL)
    3. 定期同步备份Consul/ETCD中的数据(用于容灾)
    4. 定期与其它机房的Data Control Server同步已备份的数据,用于跨机房请求加速和fallback

saas支持(mysql,81nginx)

  1. 对MySQL的支持
    1. 通过Mesh-Proxy filter支持MySQL的请求接管
    2. MySQL通过UDI+MySQL-Auth中间件支持MySQL的鉴权
    3. MySQL的统计、日志、tracing等功能复用框架提供的功能
  2. 对81nginx的支持
    1. mesh支持HTTP协议
    2. 作为python/golang HTTP服务的前置服务,以sidecar的方式存在
    3. 支持统一的metrics/log/logid
    4. 支持统一的限流(python和golang都存在大QPS下限制不住的风险)

HA

  1. control panel容灾
    control panel是中心化的有状态服务, 依赖底层数据存储(MySQL, ETCD)和Consul服务发现。同时接受来自mesh-proxy的请求和MS服务治理平台的请求。
    1. 数据层的容灾
      (1) ETCD/Consul数据定时备份
      (2) 出现故障时,由control panel接管服务注册和规则数据存储
    2. 服务层的容灾
      (1) 多机部署提供水平扩展能力
      (2) 多机房部署, 通过TLB进行切换
      (3) mesh-proxy提供对control panel结果的缓存,当control panel挂掉时,缓存不再更新
  2. mesh-proxy容灾
    本机mesh-proxy挂掉时,服务可通过动态配置,切换到其他机器的proxy

grpc & thrift

区别:

Grpc

  • Grpc 是高性能,通用的开源RPC框架,基于HTTP/2协议标准
  • Grpc 以protobuf作为LDL(接口描述语言),通过protoc来编译框架代码
  • 支持 C, C++, Node.js, Python, Ruby, Objective-C,PHP and C#

Thrift

  • Thrift是一种可伸缩的跨语言服务的RPC软件框架。它结合了功能强大的软件堆栈的代码生成引擎,以建设服务,高效、无缝地在多种语言间结合使用
  • Thrift 以thrift 作为LDL
  • 支持C、C++ 、C# 、D 、Delphi 、Erlang 、Go 、Haxe 、Haskell 、Java 、JavaScript 、node.js 、OCaml 、Perl 、PHP 、Python 、Ruby 、SmallTalk
  • 使用Thrift:Hadoop、HBase、Cassandra、Scribe、LastFM、Facebook、 Evernot

212fa7840ba2b9f758d4ff7e4df45f0e.png

b9c830ec16a797eedd532f60daa64fa7.png

什么时候应该选择gRPC而不是Thrift

需要良好的文档、示例

喜欢、习惯HTTP/2、ProtoBuf

对网络传输带宽敏感

什么时候应该选择Thrift而不是gRPC

需要在非常多的语言间进行数据交换

对CPU敏感

协议层、传输层有多种控制要求

需要稳定的版本

不需要良好的文档和示例

Redis

定制化的数据结构

字符串(SDS):

字符串保存自身长度,查询长度只需O(1)复杂度;

字符串拼接操作不会带来数组越界异常;

修改字符串不需要重新分配内存空间;

可以保存二进制数据;

SDS结构:

cc73713154e0597129a2c2deba9a94f8.png

C字符串和SDS之间的区别

dcb112e54460c56545597bca68611c4a.png

SDS特点:

  • 常数复杂度获取字符串长度
  • 杜绝修改字符串造成的缓冲区溢出
  • 减少修改字符串时带来的内存重分配次数
  • 二进制安全(可以存储’\0’,而c语言字符串不可以)
  • 兼容部分C字符串函数

list

双端链表,一个list可以存储不同类型的值

list结构为链表提供了表头指针head、表尾指针tail,以及链表长度计数器len,而dup、free和match成员则是用于实现多态链表所需的类型特定函数:·dup函数用于复制链表节点所保存的值;

·free函数用于释放链表节点所保存的值;

·match函数则用于对比链表节点所保存的值和另一个输入值是否相等。

610505f154a11fc3298ea23367df782e.png

Redis的链表实现的特性可以总结如下:

  • 双端:链表节点带有prev和next指针,获取某个节点的前置节点和后置节点的复杂度都是O(1)。
  • 无环:表头节点的prev指针和表尾节点的next指针都指向NULL,对链表的访问以NULL为终点。
  • 带表头指针和表尾指针:通过list结构的head指针和tail指针,程序获取链表的表头节点和表尾节点的复杂度为O(1)。
  • 带链表长度计数器:程序使用list结构的len属性来对list持有的链表节点进行计数,程序获取链表中节点数量的复杂度为O(1)。
  • 多态:链表节点使用void*指针来保存节点值,并且可以通过list结构的dup、free、match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。

跳跃表(skip list):

有序链表。实现简单,查找快速,平均操作复杂度O(logN)。

字典(dict):

等同于Map。哈希表(dictht)的数据结构为数组链表。

渐进式Rehash:一个dict通常维护了两个哈希表,dictht0作为正常使用的,dictht1作为rehash的临时表。rehash的过程实际上是渐进的,不会一次全部rehash完。比如在增加一个entry时,如果dict在rehash过程中,新增的key会写入到dictht1中,同事dict会将1个entry从dictht0迁移到dictht1,由dict.rehashidx标记rehash的位置。

优雅地实现扩容,减少扩容带来的阻塞。

哈希表结构:

d72a3defbcfabf02426430f29499a6a5.png

字典结构:

be45f7c857a3fb65de17b177572e1df8.png

压缩列表(ziplist):

使用二进制方式保存字符串或整数,节省内存。是列表对象和哈希对象的实现方式之一。

当一个列表键只包含少量列表项, 并且每个列表项要么就是小整数值, 要么就是长度比较短的字符串, 那么 Redis 就会使用压缩列表来做列表键的底层实现。

当一个哈希键只包含少量键值对, 并且每个键值对的键和值要么就是小整数值, 要么就是长度比较短的字符串, 那么 Redis 就会使用压缩列表来做哈希键的底层实现。

ziplist结构:

总字节数-为节点指针偏移量-节点数-节点…-结束符

ziplist-entry结构:

<encoding&length>

上一个entry大小-数据类型以及长度-数据内容

如果前一个节点长度小于254字节,prev_entry_bytes_length占1字节;否则占5字节,第一个字节用0xFE标记

以上数据结构保证ziplist可以正向遍历,也可以反向遍历。

连锁更新:由于prev_entry_bytes_length特点,插入一个新的节点时,可能导致后面的节点prev_entry_bytes_length值变大,导致后面节点也恰好超过254字节,导致连锁更新,导致插入节点的最差复杂度为O(n^2)。但平均复杂度还是O(N)

整数集合(intset):

数据结构为有序的int8_t数组,一个或多个int8_t可以组合为int8,int16,int32,int64。所以添加一个元素的时间复杂度是O(N)

升级:如果新的int值超过了最开始定义的元素类型范围,则可以对intset进行数据类型升级

对象

在以上数据结构上,redis基于它们构建了对象系统(字符串对象,列表对象,哈希对象,集合对象,有序集合对象),实现了引用计数器回收机制、访问时间记录等。

对象类型与数据结构对应关系。可以看出每种对象都有多种实现方式。

类型 编码 对象
REDIS_STRING REDIS_ENCODING_INT 使用整数值实现的字符串对象。
REDIS_STRING REDIS_ENCODING_EMBSTR 使用embstr编码的简单动态字符串实现的字符串对象。
REDIS_STRING REDIS_ENCODING_RAW 使用简单动态字符串实现的字符串对象。
REDIS_LIST REDIS_ENCODING_ZIPLIST 使用压缩列表实现的列表对象。
REDIS_LIST REDIS_ENCODING_LINKEDLIST 使用双端链表实现的列表对象。
REDIS_HASH REDIS_ENCODING_ZIPLIST 使用压缩列表实现的哈希对象。
REDIS_HASH REDIS_ENCODING_HT 使用字典实现的哈希对象。
REDIS_SET REDIS_ENCODING_INTSET 使用整数集合实现的集合对象。
REDIS_SET REDIS_ENCODING_HT 使用字典实现的集合对象。
REDIS_ZSET REDIS_ENCODING_ZIPLIST 使用压缩列表实现的有序集合对象。
REDIS_ZSET REDIS_ENCODING_SKIPLIST 使用跳跃表和字典实现的有序集合对象。

引用计数与内存回收

refcount:

redisobject使用refcount字段来记录引用次数,来判断是否要回收此段内存。

最明显的常见是redis对象共享。redis在初始化服务器时,会创建1万个字符串对象,包括0-9999的所有整数值。当使用这些字符串对象是,服务器就会共享这些对象而不是重新创建。实际上,redis也只会对整数值字符串进行共享,原因是字符串相等的比较非常耗费cpu时间

lru:

lru字段记录key的最后一次访问时间。当服务器打开maxmenmory选项是,使用lru算法回收内存的话需要使用此字段。

Redis数据库

一个redis服务端可以维护多个redis数据库,redis client可以通过SELECT命令选择数据库。每个数据库都为一个redisDb

1
2
3
4
5
6
7
8
9
10
11
typedef struct redisDb {
dict *dict; /* The keyspace for this DB */
dict *expires; /* Timeout of keys with a timeout set */
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/
dict *ready_keys; /* Blocked keys that received a PUSH */
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
int id; /* Database ID */
long long avg_ttl; /* Average TTL, just for stats */
unsigned long expires_cursor; /* Cursor of the active expire cycle. */
list *defrag_later; /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;

dict:键空间,dictEntry中key为字符串对象指针

expires:键的生存时间。key为指向键空间中对应key的指针(即键空间和过期字典的键都指向同一个键对象),value为一个long long值(毫秒),对应数据库键的过期时间。

过期删除策略

redis使用惰性删除和定期删除两种策略。

  • 惰性删除:
    在访问key时判断是否已过期,如果已经过期则删除key
  • 定期删除:
    在规定时间内,分多次遍历服务器中各个数据库,从expires中按照redisDb.expires_cursor标记的位置扫描key
    两种模式:
    • ACTIVE_EXPIRE_CYCLE_FAST-快速模式方法执行的时间不会长于EXPIRE_FAST_CYCLE_DURATION毫秒。并且在EXPIRE_FAST_CYCLE_DURATION毫秒内不会再次执行。
    • ACTIVE_EXPIRE_CYCLE_SLOW-慢速模式正常执行方式,方法的执行时限为serverCron每次执行时间的一个百分比,百分比由REDIS_EXPIRELOOKUPS_TIME_PERC定义,默认是25%。
      执行时间:
    • beforesleep
    • serverCron

持久化

RDB

自动保存机制

redisServer.saveparam保存了自动执行BGSAVE的条件。每个配置包含时间和执行次数。redisServer.lastsave记录上次BGSAVE/SAVE时间,redisServer.dirty记录上次保存以来的修改数量,serverCron中会对这些值进行检查,用来判断是否需要执行BGSAVE操作。

BGSAVE会fork一个子进程进行,子进程会带有主进程数据的副本(具体地说其实是写时复制,数据发生修改前为父进程物理地址,某个进程操作某块内存页发生修改时会复制出一块新的内存页存储新数据,旧的内存页留给另一个进程)。相当于fork时刻的数据快照

RDB结构

RDB结构

AOF

步骤

  1. 命令追加:redisServer.aof_buf为aof缓冲区,所有写命令都会追加到缓冲区末尾。
  2. 文件写入:将缓冲区内容写入AOF文件的操作系统缓存中
  3. 文件同步:通过fsync或fdatasync函数将AOF文件保存到磁盘

步骤2和步骤3在flushAppendOnlyFile函数中进行,该函数会在时间事件中和文件事件结束前被调用,通过appendfsync配置来决定是否执行:

  • appendfsync always:每次执行完一个命令之后,2 和 3 都会被执行
  • appendfsync everysec(默认配置):3 原则上每隔一秒钟就会执行一次。
  • appendfsync no:每次执行完一个命令之后, 2 会执行,3 会被忽略,只会在以下任意一种情况中被执行:
    • Redis 被关闭
    • AOF 功能被关闭
    • 系统的写缓存被刷新(可能是缓存已经被写满,或者定期保存操作被执行。完成依赖 OS 的写入,一般为 30 秒左右一次)

AOF文件重写

避免aof文件无限扩大,redis会通过读取服务器当前数据库,来重写构造aof文件。

aof同样可以在子进程中进行。此时主进程会创建一个AOF重写缓冲区,保存aof异步重写期间的操作,在异步重写完成后会阻塞地将所有内容写入到新aof文件中

事件

文件事件:套接字操作的抽象。可大致理解为每次单独的客户端请求。

时间事件:redis服务器中一些需要在给定时间执行的操作。如serverCron

文件事件

基于Reactor模式

b4d52890710de38e56a44af6f2f86e74.png

  • 使用IO多路复用(select,epoll,evport,kqueue)监听多个套接字
  • 根据套接字操作产生相应的文件事件,调用关联的事件处理器处理

高并发

Redis根据操作系统类型的选择合适的IO模型,其中最让人称道的是Epoll支撑下的事件驱动模型(Reactor模型)。Epoll可以支持大量的链接(理论上线是INT类型的最大值),并以O(1)复杂度通知用户进程IO事件。这两点为Redis高并发打下了坚实的基础。

线程安全

Redis的操作都是线程安全的,大部分用户指令都是原子的。原因很简单,Redis是单线程模型。不适合在主进程执行的操作(比如说RDB、AOF重写),它选择使用子进程进行处理(子进程拷贝父进程数据)。

集群

Redis3.0之后提供Redis Cluster功能,配合redis-trib工具,使用者可以轻松搭建一个Redis集群。Redis集群支持动态扩容缩容,支持主从互备,支持自动地故障转移。

数据一致性分析

数据一致性,Redis要么明确告知客户端请求失败,要么正确响应客户端请求并且持久化结果。

单机持久化

Redis提供两种持久化方式分别是:RDB和AOF。需要说明的一点是写入文件并不代表持久化成功,还需要将文件同步到磁盘。

RDB

RDB指的是Redis将内存中的用户数据持久化到磁盘。这就注定RDB只能在一定时间间隔的情况执行。Redis支持时间间隔和数据修改次数两种维度出发RDB。当然我们也可以通过执行SAVE或者BGSAVE命令显示地持久化数据。显而易见,以上方式总是无法避免部分最新的数据无法持久化到磁盘。

AOF

AOF指的是Redis记录所有的写操作命令,并且持久化。AOF持久化有三个级别

no:AOF文件同步交给操作系统决定;

everysec:每隔一秒执行一次文件同步;

always:写入文件立即同步。

虽然always级别最消耗性能,但是它似乎能够保证数据的一致性。不幸的是,它也不能保证数据绝对的一致性。原因如下:

1.无法以事务的形式写AOF文件和执行写操作。一旦机器在写AOF文件和执行写操作中间的某一时刻崩溃,都会导致数据的不一致性。Mysql使用二阶段提交解决这个问题。

2.文件同步到磁盘过程并非原子操作。mysql同步磁盘使用”double write”解决这个问题。

主从数据一致性

Redis支持主从互备,自动地故障转移。如果主从之间能够保证数据一致性,那我们也不需要担心持久化造成的数据不一致。不幸的是主从互备并不能保证数据一致性。

  1. 宕机。虽然多台机器同时宕机的概率极低。但我们不能忽略这种可能(墨菲定律)。
  2. 网络故障。主服务器主动将所有写操作发给从服务器。当网络通信不畅时,就会出现主从不同步。即使网络恢复正常,主服务器也不会将从服务器未接收到的命令发给从服务器。Mysql主从解决方案,从服务器以发送确认信号的方式确保主从一致。
  3. 过期Key。从服务器不会主动删除过期Key。即使我们访问从服务期的过期Key,从服务器将过期key返回给客户端。
    在单机版Redis中,存在两种删除策略:
    惰性删除:服务器不会主动删除数据,只有当客户端查询某个数据时,服务器判断该数据是否过期,如果过期则删除。
    定期删除:服务器执行定时任务删除过期数据,但是考虑到内存和CPU的折中(删除会释放内存,但是频繁的删除操作对CPU不友好),该删除的频率和执行时间都受到了限制。
    在主从复制场景下,为了主从节点的数据一致性,从节点不会主动删除数据,而是由主节点控制从节点中过期数据的删除。由于主节点的惰性删除和定期删除策略,都不能保证主节点及时对过期数据执行删除操作,因此,当客户端通过Redis从节点读取数据时,很容易读取到已经过期的数据。
    Redis 3.2中,从节点在读取数据时,增加了对数据是否过期的判断:如果该数据已过期,则不返回给客户端;将Redis升级到3.2可以解决数据过期问题。
  4. 延迟与不一致问题
    由于主从复制的命令传播是异步的,延迟与数据的不一致不可避免。如果应用对数据不一致的接受程度程度较低,可能的优化措施包括:优化主从节点之间的网络环境(如在同机房部署);监控主从节点延迟(通过offset)判断,如果从节点延迟过大,通知应用不再通过该从节点读取数据;使用集群同时扩展写负载和读负载等。
    在命令传播阶段以外的其他情况下,从节点的数据不一致可能更加严重,例如连接在数据同步阶段,或从节点失去与主节点的连接时等。从节点的slave-serve-stale-data参数便与此有关:它控制这种情况下从节点的表现;如果为yes(默认值),则从节点仍能够响应客户端的命令,如果为no,则从节点只能响应info、slaveof等少数命令。该参数的设置与应用对数据一致性的要求有关;如果对数据一致性要求很高,则应设置为no。

HA

主从

主从模式就是N个redis实例,可以是1主N从,也可以N主N从(N主N从则不是严格意义上的主从模式了,后续的集群模式会说到,N主N从就是N+N个redis实例。)

主从模式的一个作用是备份数据,这样当一个节点损坏(指不可恢复的硬件损坏)时,数据因为有备份,可以方便恢复。

另一个作用是负载均衡,所有客户端都访问一个节点肯定会影响Redis工作效率,有了主从以后,查询操作就可以通过查询从节点来完成。

  1. 一个Master可以有多个Slaves,可以是1主N从。
  2. 默认配置下,master节点可以进行读和写,slave节点只能进行读操作,写操作被禁止(readonly)。
  3. 不要修改配置让slave节点支持写操作,没有意义,原因一,写入的数据不会被同步到其他节点;原因二,当master节点修改同一条数据后,slave节点的数据会被覆盖掉。
  4. slave节点挂了不影响其他slave节点的读和master节点的读和写,重新启动后会将数据从master节点同步过来。
  5. master节点挂了以后,不影响slave节点的读,Redis将不再提供写服务,master节点启动后Redis将重新对外提供写服务。
  6. 特别说明:该种模式下,master节点挂了以后,slave不会竞选成为master。

主从节点的缺点:

master节点挂了以后,redis就不能对外提供写服务了,因为剩下的slave不能成为master

哨兵Sentinel

一、Sentinel的作用:

  1. Master 状态监测
  2. 如果Master 异常,则会进行Master-slave 转换,将其中一个Slave作为Master,将之前的Master作为Slave
  3. Master-Slave切换后,master_redis.conf、slave_redis.conf和sentinel.conf的内容都会发生改变,即master_redis.conf中会多一行slaveof的配置,sentinel.conf的监控目标会随之调换

二、Sentinel的工作方式:

  1. 每个Sentinel以每秒钟一次的频率向它所知的Master,Slave以及其他 Sentinel 实例发送一个 PING 命令
  2. 如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被 Sentinel 标记为主观下线。
  3. 如果一个Master被标记为主观下线,则正在监视这个Master的所有 Sentinel 要以每秒一次的频率确认Master的确进入了主观下线状态。
  4. 当有足够数量的 Sentinel(大于等于配置文件指定的值)在指定的时间范围内确认Master的确进入了主观下线状态, 则Master会被标记为客观下线
  5. 在一般情况下, 每个 Sentinel 会以每 10 秒一次的频率向它已知的所有Master,Slave发送 INFO 命令
  6. 当Master被 Sentinel 标记为客观下线时,Sentinel 向下线的 Master 的所有 Slave 发送 INFO 命令的频率会从 10 秒一次改为每秒一次
  7. 若没有足够数量的 Sentinel 同意 Master 已经下线, Master 的客观下线状态就会被移除。
    若 Master 重新向 Sentinel 的 PING 命令返回有效回复, Master 的主观下线状态就会被移除。

Redis cluster

基础通信原理

  1. redis cluster节点间采取gossip协议进行通信
    跟集中式不同,不是将集群元数据(节点信息,故障,等等)集中存储在某个节点上,而是互相之间不断通信,保持整个集群所有节点的数据是完整的
    维护集群的元数据用得,集中式,一种叫做gossip
    集中式:好处在于,元数据的更新和读取,时效性非常好,一旦元数据出现了变更,立即就更新到集中式的存储中,其他节点读取的时候立即就可以感知到; 不好在于,所有的元数据的跟新压力全部集中在一个地方,可能会导致元数据的存储有压力
    gossip:好处在于,元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新,有一定的延时,降低了压力; 缺点,元数据更新有延时,可能导致集群的一些操作会有一些滞后
    我们刚才做reshard,去做另外一个操作,会发现说,configuration error,达成一致
  2. 10000端口
    每个节点都有一个专门用于节点间通信的端口,就是自己提供服务的端口号+10000,比如7001,那么用于节点间通信的就是17001端口
    每隔节点每隔一段时间都会往另外几个节点发送ping消息,同时其他几点接收到ping之后返回pong
  3. 交换的信息
    故障信息,节点的增加和移除,hash slot信息,等等

gossip

gossip协议

gossip协议包含多种消息,包括ping,pong,meet,fail,等等

meet: 某个节点发送meet给新加入的节点,让新节点加入集群中,然后新节点就会开始与其他节点进行通信

redis-trib.rb add-node

其实内部就是发送了一个gossip meet消息,给新加入的节点,通知那个节点去加入我们的集群

ping: 每个节点都会频繁给其他节点发送ping,其中包含自己的状态还有自己维护的集群元数据,互相通过ping交换元数据

每个节点每秒都会频繁发送ping给其他的集群,ping,频繁的互相之间交换数据,互相进行元数据的更新

pong: 返回ping和meet,包含自己的状态和其他信息,也可以用于信息广播和更新

fail: 某个节点判断另一个节点fail之后,就发送fail给其他节点,通知其他节点,指定的节点宕机了

判断节点宕机

如果一个节点认为另外一个节点宕机,那么就是pfail,主观宕机

如果多个节点都认为另外一个节点宕机了,那么就是fail,客观宕机,跟哨兵的原理几乎一样,sdown,odown

在cluster-node-timeout内,某个节点一直没有返回pong,那么就被认为pfail

如果一个节点认为某个节点pfail了,那么会在gossip ping消息中,ping给其他节点,如果超过半数的节点都认为pfail了,那么就会变成fail

从节点过滤

对宕机的master node,从其所有的slave node中,选择一个切换成master node

检查每个slave node与master node断开连接的时间,如果超过了cluster-node-timeout * cluster-slave-validity-factor,那么就没有资格切换成master

这个也是跟哨兵是一样的,从节点超时过滤的步骤

从节点选举

哨兵:对所有从节点进行排序,slave priority,offset,run id

每个从节点,都根据自己对master复制数据的offset,来设置一个选举时间,offset越大(复制数据越多)的从节点,选举时间越靠前,优先进行选举

所有的master node开始slave选举投票,给要进行选举的slave进行投票,如果大部分master node(N/2 + 1)都投票给了某个从节点,那么选举通过,那个从节点可以切换成master

从节点执行主备切换,从节点切换为主节点

与哨兵比较

整个流程跟哨兵相比,非常类似,所以说,redis cluster功能强大,直接集成了replication和sentinal的功能

没有办法去给大家深入讲解redis底层的设计的细节,核心原理和设计的细节,那个除非单独开一门课,redis底层原理深度剖析,redis源码

对于咱们这个架构课来说,主要关注的是架构,不是底层的细节,对于架构来说,核心的原理的基本思路,是要梳理清晰的

总结

Redis一款优秀的缓存中间件。Redis的短板在于它无法保证数据的强一致性。如果您的业务场景对数据一致性要求很高,请不要把Redis当做DB使用。

multi vs pipeline

事务原子性

事务

MULTI, EXEC, DISCARD and WATCH 是Redis事务的基础。用来显式开启并控制一个事务,它们允许在一个步骤中执行一组命令。并提供两个重要的保证:

  1. 事务中的所有命令都会被序列化并按顺序执行。在执行Redis事务的过程中,不会出现由另一个客户端发出的请求。这保证 命令队列 作为一个单独的原子操作被执行。
  2. 队列中的命令要么全部被处理,要么全部被忽略。EXEC命令触发事务中所有命令的执行,因此,当客户端在事务上下文中失去与服务器的连接,
  3. 如果发生在调用MULTI命令之前,则不执行任何commands;
  4. 如果在此之前EXEC命令被调用,则所有的commands都被执行。

http

HTTP1.0 & HTTP1.1

HTTP1.0最早在网页中使用是在1996年,那个时候只是使用一些较为简单的网页上和网络请求上,而HTTP1.1则在1999年才开始广泛应用于现在的各大浏览器网络请求中,同时HTTP1.1也是当前使用最为广泛的HTTP协议。 主要区别主要体现在:

  • 缓存处理,在HTTP1.0中主要使用header里的If-Modified-Since,Expires来做为缓存判断的标准,HTTP1.1则引入了更多的缓存控制策略例如Entity tag,If-Unmodified-Since, If-Match, If-None-Match等更多可供选择的缓存头来控制缓存策略。
  • 带宽优化及网络连接的使用,HTTP1.0中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP1.1则在请求头引入了range头域,它允许只请求资源的某个部分,即返回码是206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。
  • 错误通知的管理,在HTTP1.1中新增了24个错误状态响应码,如409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除。
  • Host头处理,在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request)。
  • 长连接,HTTP 1.1支持长连接(PersistentConnection)和请求的流水线(Pipelining)处理,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟,在HTTP1.1中默认开启Connection: keep-alive,一定程度上弥补了HTTP1.0每次请求都要创建连接的缺点。

SPDY:HTTP1.x的优化

  • 降低延迟,针对HTTP高延迟的问题,SPDY优雅的采取了多路复用(multiplexing)。多路复用通过多个请求stream共享一个tcp连接的方式,解决了HOL blocking的问题,降低了延迟同时提高了带宽的利用率。
  • 请求优先级(request prioritization)。多路复用带来一个新的问题是,在连接共享的基础之上有可能会导致关键请求被阻塞。SPDY允许给每个request设置优先级,这样重要的请求就会优先得到响应。比如浏览器加载首页,首页的html内容应该优先展示,之后才是各种静态资源文件,脚本文件等加载,这样可以保证用户能第一时间看到网页内容。
  • header压缩。前面提到HTTP1.x的header很多时候都是重复多余的。选择合适的压缩算法可以减小包的大小和数量。
  • 基于HTTPS的加密协议传输,大大提高了传输数据的可靠性。
  • 服务端推送(server push),采用了SPDY的网页,例如我的网页有一个sytle.css的请求,在客户端收到sytle.css数据的同时,服务端会将sytle.js的文件推送给客户端,当客户端再次尝试获取sytle.js时就可以直接从缓存中获取到,不用再发请求了。

SPDY构成图:

ffa90376663c89409132c852a43c39bd.png

SPDY位于HTTP之下,TCP和SSL之上,这样可以轻松兼容老版本的HTTP协议(将HTTP1.x的内容封装成一种新的frame格式),同时可以使用已有的SSL功能。

HTTP2

  1. 二进制分帧层 (Binary Framing Layer):帧是数据传输的最小单位,以二进制传输代替原本的明文传输,原本的报文消息被划分为更小的数据帧51a5f47ceb459c1aae35411216a8ef7f.png
  2. 多路复用 (MultiPlexing)
    在一个 TCP 连接上,我们可以向对方不断发送帧,每帧的 stream identifier 的标明这一帧属于哪个流,然后在对方接收时,根据 stream identifier 拼接每个流的所有帧组成一整块数据。
    把 HTTP/1.1 每个请求都当作一个流,那么多个请求变成多个流,请求响应数据分成多个帧,不同流中的帧交错地发送给对方,这就是 HTTP/2 中的多路复用。
    流的概念实现了单连接上多请求 - 响应并行,解决了线头阻塞的问题,减少了 TCP 连接数量和 TCP 连接慢启动造成的问题
    所以 http2 对于同一域名只需要创建一个连接,而不是像 http/1.1 那样创建 6~8 个连接
  3. 服务端推送 (Server Push)
    浏览器发送一个请求,服务器主动向浏览器推送与这个请求相关的资源,这样浏览器就不用发起后续请求。
    Server-Push 主要是针对资源内联做出的优化,相较于 http/1.1 资源内联的优势:
    • 客户端可以缓存推送的资源
    • 客户端可以拒收推送过来的资源
    • 推送资源可以由不同页面共享
    • 服务器可以按照优先级推送资源
  4. Header 压缩 (HPACK):使用 HPACK 算法来压缩首部内容
  5. 应用层的重置连接
    对于 HTTP/1 来说,是通过设置 tcp segment 里的 reset flag 来通知对端关闭连接的。这种方式会直接断开连接,下次再发请求就必须重新建立连接。HTTP/2 引入 RST_STREAM 类型的 frame,可以在不断开连接的前提下取消某个 request 的 stream,表现更好。
  6. 请求优先级设置
    HTTP/2 里的每个 stream 都可以设置依赖 (Dependency) 和权重,可以按依赖树分配优先级,解决了关键请求被阻塞的问题
  7. 流量控制
    每个 http2 流都拥有自己的公示的流量窗口,它可以限制另一端发送数据。对于每个流来说,两端都必须告诉对方自己还有足够的空间来处理新的数据,而在该窗口被扩大前,另一端只被允许发送这么多数据。
    HTTP/1 的几种优化可以弃用
    合并文件、内联资源、雪碧图、域名分片对于 HTTP/2 来说是不必要的,使用 h2 尽可能将资源细粒化,文件分解地尽可能散,不用担心请求数多