0%

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 尽可能将资源细粒化,文件分解地尽可能散,不用担心请求数多

背景

DDL(Data Definition Languages)语句:数据定义语⾔,这些语句定义了不同的数据段、数据库、表、列、索引等数据库对象的定义。常⽤的语句关键字主要包括 create、drop、alter等。
在 mysql 5.5 版本以前,修改表结构如添加索引、修改列,需要锁表,期间不能写⼊,导致库上⼤量线程处于“Waiting for meta data lock”状态。

有⼀系列⼯具从⼏个⻆度解决这个问题。按历史的发展⻆度,可以归纳为

  1. ddl
  2. mysql 原⽣ online DDL
  3. pt-online-schema-change
  4. gh-ost

旧版innodb DDL

5.5版本以前,执⾏ddl主要有两种⽅式copy⽅式和inplace⽅式,inplace⽅式⼜称为(fast indexcreation)。相对于copy⽅式,inplace⽅式不拷⻉数据,因此较快。但是这种⽅式仅⽀持添加、删除⾮主键索引两种⽅式,⽽且与copy⽅式⼀样需要全程锁表,实⽤性不是很强。下⾯以加索引为例,简单介绍这两种⽅式的实现流程。

copy⽅式

  1. 新建带索引的临时表
  2. 锁原表,禁⽌DML,允许查询
  3. 将原表数据拷⻉到临时表(⽆排序,⼀⾏⼀⾏拷⻉)
  4. 进⾏rename,升级字典锁,禁⽌读写
  5. 完成创建索引操作

inplace⽅式

  1. 新建索引的数据字典
  2. 锁表,禁⽌DML,允许查询
  3. 读取聚集索引,构造新的索引项,排序并插⼊新索引
  4. 等待当前表所有已开始的只读事务提交
  5. 创建索引结束

inplace创建⼆级索引时会对原表加上⼀个共享锁,创建过程不需要重建表(no-rebuild);删除InnoDB⼆级索引只需要更新内部视图,并标记这个索引的物理空间可⽤,去掉数据库元数据上该索引的定义即可。

原生online DDL

配置

FIC只对索引的创建删除有效,MySQL 5.6 Online DDL把这种特性扩展到了添加列、删除列、修改列类型、列重命名、设置默认值等等,实际效果要看所使⽤的选项和操作类别来定。MySQL 在线DDL同样分为 INPLACE 和 COPY 两种⽅式。

  • 对于copy⽅式,⽐如修改列类型,删除主键,修改字符集等场景,这些操作都会导致记录格式发⽣变化,⽆法通过简单的全量+增量的⽅式实现online
  • 对于inplace⽅式,mysql内部以“是否修改记录格式”为基准也分为两类,⼀类需要重建表(重新组织记录) ,⽐如optimize table、添加索引、添加/删除列、修改列NULL/NOT NULL属性等;另外
    ⼀类是只需要修改表的元数据,⽐如删除索引、修改列名、修改列默认值、修改列⾃增值等。

Mysql将这两类⽅式分别称为 rebuild ⽅式和 no-rebuild ⽅式。

1
2
3
4
5
6
7
8
? alter table
| ALGORITHM [=] {DEFAULT|INPLACE|COPY}
| ALTER [COLUMN] col_name {SET DEFAULT literal | DROP DEFAULT}
| CHANGE [COLUMN] old_col_name new_col_name column_definition
[FIRST|AFTER col_name]
| LOCK [=] {DEFAULT|NONE|SHARED|EXCLUSIVE}
| MODIFY [COLUMN] col_name column_definition
[FIRST | AFTER col_name]

通过在ALTER语句的ALGORITHM参数指定。

  • ALGORITHM=INPLACE ,可以避免重建表带来的IO和CPU消耗,保证ddl期间依然有良好的性能和并发。
  • ALGORITHM=COPY ,需要拷⻉原始表,所以不允许并发DML写操作,可读。这种copy⽅式的效率还是不如 inplace ,因为前者需要记录undo和redo log,⽽且因为临时占⽤buffer pool引起短时间内性能受影响。

上⾯只是 Online DDL 内部的实现⽅式,此外还有 LOCK 选项控制是否锁表,根据不同的DDL操作类型有不同的表现:默认mysql尽可能不去锁表,但是像修改主键这样的昂贵操作不得不选择锁表。

  • LOCK=NONE ,即DDL期间允许并发读写涉及的表,⽐如为了保证 ALTER TABLE 时不影响⽤⼾注册或⽀付,可以明确指定,好处是如果不幸该 alter语句不⽀持对该表的继续写⼊,则会提⽰失败,⽽不会直接发到库上执⾏。 ALGORITHM=COPY 默认LOCK级别
  • LOCK=SHARED ,即DDL期间表上的写操作会被阻塞,但不影响读取。
  • LOCK=DEFAULT ,让mysql⾃⼰去判断lock的模式,原则是mysql尽可能不去锁表
  • LOCK=EXCLUSIVE ,即DDL期间该表不可⽤,堵塞任何读写请求。如果你想alter操作在最短的时间内完成,或者表短时间内不可⽤能接受,可以⼿动指定。

⽆论任何模式下,online ddl开始之前都需要⼀个短时间排它锁(exclusive)来准备环境,所以alter命令发出后,会⾸先等待该表上的其它操作完成,在alter命令之后的请求会出现等待waiting metadata lock。同样在ddl结束之前,也要等待alter期间所有的事务完成,也会堵塞⼀⼩段时间。所以尽量在ALTER TABLE之前确保没有⼤事务在执⾏,否则⼀样出现连环锁表。

online DDL操作类别

  • In-Place 为Yes是优选项,说明该操作⽀持INPLACE
  • Copies Table 为No是优选项,因为为Yes需要重建表。⼤部分情况与In-Place是相反的
  • Allows Concurrent DML? 为Yes是优选项,说明ddl期间表依然可读写,可以指定 LOCK=NONE(如果操作允许的话mysql⾃动就是NONE)
  • Allows Concurrent Query? 默认所有DDL操作期间都允许查询请求,放在这只是便于参考
  • Notes 会对前⾯⼏列Yes/No带 * 号的限制说明
Operation In-Place? Copies Tables? Allow Concurrent DML? Allows Concurent Query? Notes
添加索引 Yes* No* Yes Yes 对全文索引的一些限制
删除索引 Yes No Yes Yes 仅修改表的元数据
OPTIMIZE TABLE Yes Yes Yes Yes 从5.6.17开始使用ALGORITHM=INPLACE, 当然如果指定了old_alter_table=1或mysqld启动带–skip-new则将还是COPY模式。如果表上有全文索引只支持Cope
对一列设置默认值 Yes No Yes Yes 仅修改表的元数据
对一列修改auto-increment的值 Yes No Yes Yes 仅修改表的元数据
添加foreign key constraint Yes No* Yes Yes 为了避免拷贝表,在约束创建时会禁用foreign_key_checks
删除foreign key constraint Yes No Yes Yes foreign_key_checks不影响
改变列名 Yes* No* Yes* Yes 为了允许DML并发,如果保持相同数据类型,仅改变列名
添加列 Yes* Yes* Yes* Yes 尽管允许ALGORITHM=INPLACE,但数据大幅重组,所以它仍然是一项昂贵的操作。当添加列是auto-increment,不允许DML并发
删除列 Yes Yes* Yes Yes 修改类型或添加长度,都会拷贝表,而且不允许更新操作
修改列数据类型 No Yes* No Yes 尽管允许ALGORITHM=INPLACE,但数据大幅重组,所以它仍然是一项昂贵的操作。
更改列顺序 Yes Yes Yes Yes 尽管允许ALGORITHM=INPLACE,但数据大幅重组,所以它仍然是一项昂贵的操作。
修改ROW-FORMAT和KEY_BLOCK_SIZE Yes Yes Yes Yes 尽管允许ALGORITHM=INPLACE,但数据大幅重组,所以它仍然是一项昂贵的操作。
没置列属性NULL或NOT NULL Yes Yes Yes Yes 尽管允许ALGORITHM=INPLACE,但数据大幅重组,所以它仍然是一项昂贵的操作。如果列定义必须转化NOT NULL,则不允许INPLACE
添加主键 Yes* Yes Yes Yes 在同一个 ALTER TABLE 语句删除就主键、添加新主键时,才允许inplace;数据大幅重组,所以它仍然是一项昂贵的操作。
删除并添加主键 Yes Yes Yes Yes 在同一个ALTER TABLE语句删除旧主键、添加新主键时,才允许INPLACE;数据大幅重组,所以它仍然是一项昂贵的操作。
删除主键 No Yes No Yes 不允许并发DML,要拷贝表,而且如果没有在同一ATLER TABLE 语句里同时添加主键则会收到限制
变更表字符集 No Yes No Yes 如果新的字符集编码不同,重建表

规则总结

  • In-Place为No,DML⼀定是No,说明 ALGORITHM=COPY ⼀定会发⽣拷⻉表,只读。
  • ALGORITHM=INPLACEE 也要可能发⽣拷⻉表,但可以并发DML:
    • 添加、删除列,改变列顺序
    • 添加或删除主键
    • 改变⾏格式ROW_FORMAT和压缩块⼤⼩KEY_BLOCK_SIZE
    • 改变列NULL或NOT NULL
    • 优化表OPTIMIZE TABLE
    • 强制 rebuild 该表
  • 不允许并发DML的情况有
    • 修改列数据类型
    • 删除主键
    • 变更表字符集
  • 更改聚集索引,体现了表数据在物理磁盘上的排列,包含了数据⾏本⾝,需要拷⻉表;⽽普通索引通过包含主键列来定位数据,所以普通索引的创建只需要⼀次扫描主键即可,⽽且是在已有数据的表上建⽴⼆级索引,更紧凑,将来查询效率更⾼。
  • 修改主键也就意味着要重建所有的普通索引。

执⾏流程

  • Prepare阶段 :

    1. 创建新的临时frm⽂件(与InnoDB⽆关)
    2. 持有EXCLUSIVE-MDL锁,禁⽌读写
    3. 根据alter类型,确定执⾏⽅式(copy,online-rebuild,online-norebuild)
    4. 更新数据字典的内存对象
    5. 分配row_log对象记录增量(仅rebuild类型需要)
    6. ⽣成新的临时ibd⽂件(仅rebuild类型需要)
  • ddl执⾏阶段 :

    1. 降级EXCLUSIVE-MDL锁,允许读写
    2. 扫描old_table的聚集索引每⼀条记录rec
    3. 遍历新表的聚集索引和⼆级索引,逐⼀处理
    4. 根据rec构造对应的索引项
    5. 将构造索引项插⼊sort_buffer块排序
    6. 将sort_buffer块更新到新的索引上
    7. 记录ddl执⾏过程中产⽣的增量(仅rebuild类型需要)
    8. 重放row_log中的操作到新索引上(no-rebuild数据是在原表上更新的)
    9. 重放row_log间产⽣dml操作append到row_log最后⼀个Block
  • commit阶段 :

    1. 当前Block为row_log最后⼀个时,禁⽌读写,升级到EXCLUSIVE-MDL锁
    2. 重做row_log中最后⼀部分增量
    3. 更新innodb的数据字典表
    4. 提交事务(刷事务的redo⽇志)
    5. 修改统计信息
    6. rename临时idb⽂件,frm⽂件
    7. 变更完成

⼏个疑问点

  1. 如何实现数据完整性
    row_log记录了ddl变更过程中新产⽣的dml操作,并在ddl执⾏的最后将其应⽤到新的表中,保证数据完整性。
  2. online与数据⼀致性如何兼得
    online ddl在prepare阶段和commit阶段都会持有MDL-Exclusive锁,禁⽌读写。由于prepare和commit阶段相对于ddl执⾏阶段时间特别短,因此基本可以认为是全程online的。Prepare阶段和commit阶段的禁⽌读写,主要是为了保证数据⼀致性。Prepare阶段需要⽣成row_log对象和修改内存的字典;Commit阶段,禁⽌读写后,重做最后⼀部分增量,然后提交,保证数据⼀致。
  3. 如何实现server层和innodb层⼀致性
    在prepare阶段,server层会⽣成⼀个临时的frm⽂件,⾥⾯包含了新表的格式;innodb层⽣成了临时的ibd⽂件(rebuild⽅式);在ddl执⾏阶段,将数据从原表拷⻉到临时ibd⽂件,并且将row_log增量应⽤到临时ibd⽂件;在commit阶段,innodb层修改表的数据字典,然后提交;最后innodb层和mysql层⾯分别重命名frm和idb⽂件。
  4. 主从模式下的延时
    在主从环境下,主库执⾏alter命令在完成之前是不会进⼊binlog记录事件,如果允许dml操作则不影响记录时间,所以期间不会导致延迟。然⽽,由于从库是单个SQL Thread按顺序应⽤relay log,轮到ALTER语句时直到执⾏完才能下⼀条,所以从库会在master ddl完成后开始产⽣延迟。如果主库ddl使⽤了1⼩时,从库将产⽣1⼩时的主从延时。(pt-osc可以控制延迟时间,所以这种场景下它更合适)

pt-online-schema-change

online DDL还是有⼀段锁表时间,这段时间的查询依然会引起meta data lock问题。pt-osc解决了这个问题,有在线修改表不锁表的能⼒。percona公司

pt-osc⼯作过程

  1. 创建⼀个和要执⾏ alter 操作的表⼀样的新的空表结构(是alter之前的结构)
  2. 在新表执⾏alter table 语句(速度应该很快)
  3. 在原表中创建触发器3个触发器分别对应insert,update,delete操作
    1
    2
    3
    6165 Query CREATE TRIGGER `pt_osc_confluence_sbtest3_del` AFTER DELETE ON `confluence`.`sbtest3` FOR EACH ROW DELETE IGNORE FROM `confluence`.`_sbtest3_new` WHERE `confluence`.`_sbtest3_new`.`id` <=> OLD.`id`
    6165 Query CREATE TRIGGER `pt_osc_confluence_sbtest3_upd` AFTER UPDATE ON `confluence`.`sbtest3` FOR EACH ROW REPLACE INTO `confluence`.`_sbtest3_new`(`id`, `k`, `c`, `pad`) VALUES (NEW.`id`, NEW.`k`, NEW.`c`, NEW.`pad`)
    6165 Query CREATE TRIGGER `pt_osc_confluence_sbtest3_ins` AFTER INSERT ON `confluence`.`sbtest3` FOR EACH ROW REPLACE INTO `confluence`.`_sbtest3_new`(`id`, `k`, `c`, `pad`) VALUES (NEW.`id`, NEW.`k`, NEW.`c`, NEW.`pad`)
    replace into :
    a. 如果发现表中已经有此⾏数据(根据主键或者唯⼀索引判断)则先删除此⾏数据,然后插⼊新的数据。
    b. 否则,直接插⼊新数据。
  4. 以⼀定块⼤⼩从原表拷⻉数据到临时表,拷⻉过程对数据⾏持有S锁。拷⻉过程中通过原表上的触发器在原表进⾏的写操作都会更新到新建的临时表。
    1
    2
    3
    6165 Query INSERT LOW_PRIORITY IGNORE INTO `confluence`.`_sbtest3_new`(`id`, `k`, `c`, `pad`) SELECT `id`, `k`, `c`, `pad` FROM `confluence`.`sbtest3` FORCE INDEX(`PRIMARY`) WHERE ((`id` >= '4692805')) AND ((`id` <= '4718680')) LOCK IN SHARE MODE /*pt-online-schema-change 46459 copy nibble*/
    INSERT IGNORE 与INSERT INTO的区别就是INSERT IGNORE会忽略数据库中已经存在 的数据,如果数据库没有数据,就插⼊新的数据,如果有数据的话就跳过这条数据。这样就可以保留数据库中已经存在数据,达到在间隙中插⼊数据的⽬的。
    如果数据修改的时候,还没有拷⻉到新表,修改后再拷⻉,虽然重复覆盖,但是数据也没有出错;如果是数据已经拷⻉,原表发⽣修改,这时触发器同步修改数据,两种情况下都保证了数据的⼀致性;
  5. Rename 原表到old表中,在把临时表Rename为原表
  6. 如果有参考该表的外键,根据alter-foreign-keys-method参数的值,检测外键相关的表,做相应设置的处理
    alter-foreign-keys-method配置:
    • rebuild_constraints ,优先采⽤这种⽅式
      • 它先通过 alter table t2 drop fk1,add _fk1 重建外键参考,指向新表
      • 再 rename t1 t1_old, _t1_new t1 ,交换表名,不影响客⼾端删除旧表 t1_old
      • 但如果字表t2太⼤,以致alter操作可能耗时过⻓,有可能会强制选择 drop_swap。
      • 涉及的主要⽅法在 pt-online-schema-change ⽂件的determine_alter_fk_method, rebuild_constraints, swap_tables三个函数中。
    • drop_swap ,
      • 禁⽤t2表外键约束检查 FOREIGN_KEY_CHECKS=0
      • 然后 drop t1 原表
      • 再 rename _t1_new t1
      • 这种⽅式速度更快,也不会阻塞请求。但有⻛险,第⼀,drop表的瞬间到rename过程,原表t1是不存在的,遇到请求会报错;第⼆,如果因为bug或某种原因,旧表已删,新表rename失败,那就太晚了,但这种情况很少⻅。
      • 我们的开发规范决定,即使表间存在外键参考关系,也不通过表定义强制约束。
  7. 默认最后将旧原表删除
  8. 从库通过binlog执⾏相同操作

与原⽣online ddl⽐较

  • online ddl在必须copy table时成本较⾼,不宜采⽤
  • pt-osc⼯具在表存在触发器时,不适⽤(⼀个表上不能同时有2个相同类型的触发器)
  • 修改索引、外键、列名时,优先采⽤online ddl,并指定 ALGORITHM=INPLACE
  • 其它情况使⽤pt-osc,虽然存在copy data
  • pt-osc⽐online ddl要慢⼀倍左右,因为它是根据负载调整的
  • ⽆论哪种⽅式都选择的业务低峰期执⾏
  • 特殊情况需要利⽤主从特性,先alter从库,主备切换,再改原主库

gh-ost

why gh-ost

基于触发器的online ddl⽅案的不⾜

  • Triggers, overhead: 触发器是⽤存储过程的实现的,就⽆法避免存储过程本⾝需要的开销。
  • Triggers, locks: 增⼤了同⼀个事务的执⾏步骤,更多的锁争抢。
  • Trigger based migration, no pause: 整个过程⽆法暂停,假如发现影响主库性能,停⽌ OnlineDDL,那么下次就需要从头来过。
  • Triggers, multiple migrations: 多个并⾏的操作是不安全的。
  • Trigger based migration, no reliable production test: ⽆法在⽣产环境做测试。
  • Trigger based migration, bound to server: 触发器和源操作还是在同⼀个事务空间。

gh-ost优点:

  • ⽆触发器
    gh-ost 希望⼆进制⽂件使⽤基于⾏的⽇志格式。也可以使⽤从库,将基于语句的⽇志格式转化成基于⾏的⽇志格式。
  • 轻量级
    因为不需要使⽤触发器,gh-ost 把修改表定义的负载和正常的业务负载解耦开了。它不需要考虑被修改的表上的并发操作和竞争等,这些在⼆进制⽇志中都被序列化了,gh-ost 只操作临时表,完全与原始表不相⼲。事实上,gh-ost 也把⾏拷⻉的写操作与⼆进制⽇志的写操作序列化了,这样,对主库来说只是有⼀条连接在顺序的向临时表中不断写⼊数据,这样的⾏为与常⻅的 ETL 相当不同。
  • 并⾏操作
    对于 gh-ost 来说就是多个对主库的连接来进⾏写操作。
  • 可暂停
    因为所有写操作都是 gh-ost ⽣成的,⽽读取⼆进制⽂件本⾝就是⼀个异步操作,所以在暂停时,gh-ost 是完全可以把所有对主库的写操作全都暂停的。
  • 动态可控
    gh-ost 通过监听 TCP 或者 unix socket ⽂件来获取命令。即使有正在进⾏中的修改⼯作,⽤⼾也可以向 gh-ost 发出命令修改配置
  • 可审计
    和动态可控⼀样,可以通过命令查看任务进度、参数、实例情况等。
  • 可测试
  • 可靠
    综合以上⼏个特性。它是可靠的。

原理

gh-ost 作为⼀个伪装的备库,可以从主库/备库上拉取 binlog,过滤之后重新应⽤到主库上去,相当于主库上的增量操作通过 binlog ⼜应⽤回主库本⾝,不过是应⽤在幽灵表上。
image-004.png

  1. gh-ost ⾸先连接到主库上,根据 alter 语句创建幽灵表
  2. 然后作为⼀个”备库“连接到其中⼀个真正的备库上,⼀边在主库上拷⻉已有的数据到幽灵表,⼀边从备库上拉取增量数据的 binlog,然后不断的把 binlog 应⽤回主库。
  3. cut-over 是最后⼀步,锁住主库的源表,等待 binlog 应⽤完毕,然后替换 gh-ost 表为源表。gh-ost 在执⾏中,会在原本的 binlog event ⾥⾯增加以下 hint 和⼼跳包,⽤来控制整个流程的进度,检测状态等。

⼏种模式

image-005.png
作者并没有对着三种模式有明确的使⽤倾向

模式⼀、连上从库,在主库上修改

这是 gh-ost 默认的⼯作模式,它会查看从库情况,找到集群的主库并且连接上去。修改操作的具体步骤是:

  • 在主库上读写⾏数据;
  • 在从库上读取⼆进制⽇志事件,将变更应⽤到主库上;
  • 在从库上查看表格式、字段、主键、总⾏数等;
  • 在从库上读取 gh-ost 内部事件⽇志(⽐如⼼跳);
  • 在主库上完成表切换;

如果主库的⼆进制⽇志格式是 Statement,就可以使⽤这种模式。但从库就必须配成启⽤⼆进制⽇志(log_bin, log_slave_updates),还要设成 Row 格式(binlog_format=ROW),实际上 gh-ost 会在从库上帮你做这些设置。
事实上,即使把从库改成 Row 格式,这仍然是对主库侵⼊最少的⼯作模式。

模式⼆、直接在主库上修改

如果没有从库,或者不想在从库上操作,那直接⽤主库也是可以的。gh-ost 就会在主库上直接做所有的操作。仍然可以在上⾯查看主从复制延迟。

  • 主库必须产⽣ Row 格式的⼆进制⽇志;
  • 启动 gh-ost 时必须⽤–allow-on-master 选项来开启这种模式;

模式三、在从库上修改和测试

这种模式会在从库上做修改。gh-ost 仍然会连上主库,但所有操作都是在从库上做的,不会对主库产⽣任何影响。在操作过程中,gh-ost 也会不时地暂停,以便从库的数据可以保持最新。

  • –migrate-on-replica 选项让 gh-ost 直接在从库上修改表。最终的切换过程也是在从库正常复制的状态下完成的。
  • –test-on-replica 表明操作只是为了测试⽬的。在进⾏最终的切换操作之前,复制会被停⽌。原始表和临时表会相互切换,再切换回来,最终相当于原始表没被动过。主从复制暂停的状态下,你可以检查和对⽐这两张表中的数据。

时序问题分析

binlog执⾏时间和copy old row时序不会影响最终结果。分析如下:
执⾏器对binlog语句实际执⾏时动作的替换

源类型 目标类型
insert replace
update update
delete delete

对与insert和update是没有问题的,因为⽆论copy old row和apply binlog的先后顺序,如果applybinlog在后,会覆盖掉copy old row,如果apply binlog在前⾯,copy old row因为使⽤insert ignore,因此会被ignore掉;

对与delete数据,abc三个操作,可能存在三种情况(b肯定在a的后⾯):
a.delete old row
b.delete binlog apply
c.copy old row

  1. cab,c会将数据copy到ghost表,最后b会把ghost表中的数据delete掉;
  2. acb,c空操作,b也是空操作;
  3. abc,b空操作,c也是空操作;

参考⽂档

ONLINE DDL VS PT-ONLINE-SCHEMA-CHANGE
gh-ost triggerless design
sbr vs rbr

用户态 内核态

用户态 内核态

Linux探秘之用户态与内核态

6dd7be5598bfd5a51a42ff570813d614.png

内核从本质上看是一种软件——控制计算机的硬件资源,并提供上层应用程序运行的环境。用户态即上层应用程序的活动空间,应用程序的执行必须依托于内核提供的资源,包括CPU资源、存储资源、I/O资源等。为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用。

因为操作系统的资源是有限的,如果访问资源的操作过多,必然会消耗过多的资源,而且如果不对这些操作加以区分,很可能造成资源访问的冲突。所以,为了减少有限资源的访问和使用冲突,Unix/Linux的设计哲学之一就是:对不同的操作赋予不同的执行等级,就是所谓特权的概念。简单说就是有多大能力做多大的事,与系统相关的一些特别关键的操作必须由最高特权的程序来完成。Intel的X86架构的CPU提供了0到3四个特权级,数字越小,特权越高,Linux操作系统中主要采用了0和3两个特权级,分别对应的就是内核态和用户态。运行于用户态的进程可以执行的操作和访问的资源都会受到极大的限制,而运行在内核态的进程则可以执行任何操作并且在资源的使用上没有限制。很多程序开始时运行于用户态,但在执行的过程中,一些操作需要在内核权限下才能执行,这就涉及到一个从用户态切换到内核态的过程。比如C函数库中的内存分配函数malloc(),它具体是使用sbrk()系统调用来分配内存,当malloc调用sbrk()的时候就涉及一次从用户态到内核态的切换,类似的函数还有printf(),调用的是wirte()系统调用来输出字符串,等等。

中断 异常 系统调用

中断 异常 系统调用

  • 系统调用 (system call)(软中断)
    应用程序 主动 向操作系统发出的服务器请求
  • 异常 (exception)
    非法指令或其他原因导致当前 指令执行失败
    如: 内存出错后的处理请求
  • 中断(hardware interrupt)
    来自硬件设备的处理请求

从触发方式上看,可以认为存在前述3种不同的类型,但是从最终实际完成由用户态到内核态的切换操作上来说,涉及的关键步骤是完全一致的,没有任何区别,都相当于执行了一个中断响应的过程,因为系统调用实际上最终是中断机制实现的,而异常和中断的处理机制基本上也是一致的。涉及到由用户态切换到内核态的步骤主要包括:

  1. 从当前进程的描述符中提取其内核栈的ss0及esp0信息。
  2. 使用ss0和esp0指向的内核栈将当前进程的cs,eip,eflags,ss,esp信息保存起来,这个过程也完成了由用户栈到内核栈的切换过程,同时保存了被暂停执行的程序的下一条指令。
  3. 将先前由中断向量检索得到的中断处理程序的cs,eip信息装入相应的寄存器,开始执行中断处理程序,这时就转到了内核态的程序执行了。

异步io

异步IO

线程状态流转