可伸缩服务架构-框架与中间件(读书笔记一)
第一章
如何设计一款用不重复的高性能分布式发号器
https://github.com/iweisi/vesta-id-generator
第二章
可灵活扩展的消息队列框架的设计与实现
2.1 背景介绍
消息队列多应用于异步处理、模块之间解耦和高并发系统的肖峰等场景。
kafka是使用支持高并发的Scala语言开发的,利用操作系统的缓存原理达到高性能,并且天生具有可分区、分布式等特点。
开发一个消息队列的目标
1、简单易用,在使用方面,提供多种使用方式,:直接使用Java API,与spring环境无缝集成,服务源码注解,通过注解声明方式启动kafka消息队列的处理机。
2、高性能,使用不同的线程池技术保证处理机的高性能,适合轻量级服务的同步线程模型及适合I/O密集型服务的异步线程模型,在异步线程模型中的线程池也支持有确定的线程数量的线程池和线程数量可自动增减的线程池。
3、高稳定性,系统发生错误时,可恢复或清洗数据,实现优雅关机和重启。
4、架构难点
4.1、线程模型
4.1.1、同步线程模型,客户端为每个消费者流使用一个线程,每个线程负责从队列消费消息,并且在同一个线程里处理业务,这种模型多用于处理轻量级别的业务。
4.1.2、异步线程模型,客户端为每个消费者流使用一个线程,每个线程负责从队列消费消息,,并且传递消费到的消息到后端的异步线程池中,在异步线程池中处理业务。我们把前面负责消费消息的线程池称为消息消费线程池,把后面的异步线程池称为异步业务处理线程池,这种模型适合重量级的业务,如业务中有大量的I/O操作、网络I/O操作、复杂计算、对外部系统的调用等。
1)所有消费者流共享线程池
2)每个流独享线程池
4.2、异常处理,对异常处理采用监听者模式来实现异常处理器的可插拔
4.3、优雅关机,重点:
— 如何知道JVM要退出?(注册JVM退出钩子)
— 如何阻止Daemon线程在JVM退出后被杀掉?(等待Worker线程退出后再退出)
— 如果Worker线程处理阻塞状态,则如何唤醒并退出?(在指定退出时间内没有退出,则中断线程以退出)
第三章
轻量级的数据库分库分表架构与框架
3.1、什么是分库分表?
为了分散数据库压力,我们会将一个表结构氛围多个表,或者将一个表的数据分片后放入多个表,这些表可以放在同一个库,也可以放在不同的库。
垂直拆分:根据业务的维度,将原本的一个库(表)拆分为多个库(表),每个库(表)与原有结构不同
水平拆分:根据分片算法,将一个库(表)拆分为多个库(表),每个库(表)与原有结构相同
3.1.1、使用数据库的三个阶段
1、单库单表
2、单库多表
3、多库多表
3.1.2、在什么情况下需要分库分表
1、一个库中表数据超过了一定的数量(MySQL单表数据达到千万级别)
2、如果数据库的吞吐量达到了瓶颈
3、希望在扩容时对应用层的配置改变最少,就需要再每个数据库实例中预留足够的数据库容量
3.2、三种分而治之的解决方案
3.2.1、客户端分片
3.2.1.1、在应用层直接实现,这种非常通用,简单。一般公司会将这些逻辑进行封装,打包成一个jar包供公司内部项目使用。这种方案虽然入侵了业务,但是实现简单,适合快速上线,由于分片逻辑是自己实现的,出现问题,容易定位,容易解决。
3.2.1.2、通过订制JDBC协议实现,这种解决方案对业务透明,不入侵业务,让开发和分库分表的配置人员在一定程度上可以分离。流行的客户端分表框架Sharding JDBC便采用了这种方案
3.2.1.3、通过定制ORM框架实现,在很多公司里通过在Mybatis配置文件的SQL中增加表索引的参数来实现分片,如:
select * from user_#{index} where user_id=#{userId}
3.2.2、代理分片
在应用层和数据库层增加一个代理层,把分片的路由规则配置在代理层,代理层对外提供与JDBC兼容的接口给应用层,应用层不关心分片规则,只需在代理层配置路由规则即可。这种方案维护成本高。常用的框架有Cobar和Mycat。
3.2.3、支持事务的分布式数据库
如OceanBase、TiDB,对外提供可伸缩的体系架构,并提供一定的分布式事务支持,将可伸缩的特点和分布式事务的实现包装到分布式数据库内部实现,对使用者透明。
3.3、分库分表的架构设计
3.3.1、整体切分方式
垂直切分就是根据每个表的不同业务进行切分
对配置表的某些字段很少进行修改时,将其放到一个查询性能较高的数据库硬件上,对配置的其他字段更新频繁时,则将其放到另外一个更新性能较高的数据库硬件上。
在MySQL数据库中,冷数据查询较多,适合用MyISAM引擎,而热数据更新频繁,适合使用InnoDB存储引擎。
读多写少的冷数据可配置更多的从库来化解大量查询请求的压力,对于热数据,可以使用多个主库分库分表的结构。
垂直切分和水平切分的共同点:
— 存在分布式事务问题
— 存在跨节点join问题
— 存在跨节点合并、排序、分页问题
— 存在多数据源管理问题
3.3.2、水平切分的路由过程和分片维度
通过分库分表的规则找到对应的表和库的过程叫做路由
水平切片的分片维度
1)按照哈希切片
对数据的某个字段求哈希,再除以分片总数后取模,取模后相同的数据为一个分片。在设计时,要充分考虑如何设计数据库的分库分表的路由规则
2)按照时间分片
适用于有明显时间特点的数据
3.3.3、分片后的事务处理机制
1、分布式事务
— 两阶段提交协议(准备阶段,提交阶段,性能较差,很难水平扩展)
— 最大努力保证模式(适用于对数据一致性要求不太严格的场景)
样例
(1)开始消息事务
(2)开始数据库事务
(3)接收消息
(4)更新数据库
(5)提交数据库事务
(6)提交消息事务
最关键的一步放在最后,如果中间出了问题,可以直接数据库回滚,消息不会丢失
— 事务补偿机制
需要记录遇到问题的环境、信息、步骤、状态,后续通过重试机制达到最终一致性,深入可以去理解ACID原理,CAP原理,BASE原理,最终一致性模式
2、事务路由
— 自动提交事务路由
— 可编程事务路由
— 声明式事务路由
3.3.4、读写分离
在DBA领域一般配置主-主-从或者主-从-从两种部署模型
所有写操作都先在主库操作,再异步更新到从库
3.3.5、分库分表引起的问题
1、扩容与迁移
— 按照新旧分片规则,双写
— 将分片前按照旧分片规则写的历史数据,按照新规则迁移写入新数据库
— 将按照旧分片规则的查询改为按照新分片规则查询
— 将双写数据库逻辑从代码中下线,只按照新的分片规则写入数据
— 删除按照旧分片规则写入的历史数据
2、分库分表维度导致的查询问题
— 在多个分片查询后合并数据,这种效率较低
— 记录两份数据,一份按照买家维度分表,一份按照商品维度分表
— 通过搜索引擎解决,但如果实时性要求很高,就需要实现实时搜索
3、跨库事务难以实现
避免同事修改多个数据库的事务
4、同组数据跨库问题
尽量把同组数据放在同一台数据库上
第四章 缓存的本质和缓存使用的优秀实践
4.1、使用缓存的目的和问题
使用缓存提高读操作性能的同时,一定会失去一定的一致性
评估一个系统是否要使用缓存时,不但要看使用缓存能否提高性能,能提高多少性能,还要看为了提高性能牺牲的数据一致性能否让用户接受。
使用缓存的场景:
— 读密集型的应用
— 存在热数据的应用
— 对响应时效要求较高的应用
— 对一致性要求不严格的应用
— 需要实现分布式锁的应用
不适合使用缓存的场景
— 读少
— 更新频繁
— 对一致性要求严格
4.2、自相似,CPU的缓存和系统的架构的缓存
4.2.3、缓存行与伪共享
一个缓存行存储字节的数量是2的倍数,缓存行的大小为32字节到256字节不等,通常设为64字节。
伪共享指的是多个线程同时读一个缓存行的不同变量时,尽管这些变量之间么有任何关系,但是多个线程之间仍然需要同步,从而导致性能下降的情况。
JVM的内存模型,所有Java对象都有8字节对象头,前4个字节用来保存对象的哈希码和锁的状态,一旦对象上锁,这4个字节都会被拿出对象外,并用指针链接,剩下的4个字节用来存储对象所属类的引用。对于数组来讲,还有一个保存数组大小的变量,共4个字节。每个对象的大小都会对齐到8字节的倍数,不够8字节的需要填充。为了保证效率,Java编译器在编译Java对象时,会通过字段类型对Java对象的字段进行排序。
排序顺序:double,long,int,float,short,char,boolean,byte,对象引用,子类字段。
我们可以在任意字段间通过填充长整型的变量,把热点变量隔离在不同的缓存行,通过减少伪共享,在多核心CPU中能够极大的提高效率。
4.2.4、从CPU的体系架构到分布式的缓存架构
CPU架构 |
系统架构 |
寄存器 |
分布式节点本地内存 |
L1缓存 |
分布式节点本地内存 |
L2缓存 |
分布式节点本地内存 |
L3缓存 |
Redis等分布式缓存 |
内存 |
数据库,Elasticsearch等数据存储 |
1、缓存高可用
一般都是基于分片的主从来实现的,通过分片来分割大数据的查询,通过主从来完成高可用和部分高性能需求,通过多副本,可以化解查询带来的压力。
2、缓存高并发,把么有依赖关系的缓存变为并行执行,保留有依赖的为串行执行
4.3、常用的分布式缓存方案
包括Redis,Memcached和阿里的Tair
Redis
1、数据类型,支持String,list,hash,set,zset
2、线程模型,单线程非阻塞网络I/O模型,适合快速的操作逻辑,官方推荐一台机器使用8个实例
3、持久机制,定时持久机制(RDB)和基于操作日志的持久机制(AOF)
4、客户端,pedis,使用阻塞I/O,可以配置连接池,并提供了一致性hash分片的逻辑
5、高可用,支持主从节点配置
6、对队列的支持,支持lpush/brpop,publish/subscribe/psubscribe等队列和订阅模式
7、事务,提供一定程度上支持线程安全和事务的命令,如multi/exec,watch,inc
8、数据淘汰策略,maxmemory,maxmemory-policy,volatile-leu,allkeys-lru,volatile-random,alleys-random,volatile-tcl,noeviction
9、内存分片,zmalloc,zfree
4.3.2、Redis初体验
1、使用场景
最容易出问题的是保存json数据,官方推荐采用hash码来存放对象
2、Redis的高可用方案:哨兵
操作集群节点的上线、下线、监控、提醒、自动故障切换,实现了著名RAFT选主协议,哨兵节点一般至少部署3个节点
3、Redis集群,不是强一致性
4.4、分布式缓存的通用方法
4.4.1、缓存编程的具体方法
1、编程法
2、spring注入法(spring-data-redis)
3、注解法
4.4.2、应用层访问缓存的模式
1、双读双写,对于读操作,我们先读缓存,如果缓存不存在这条数据,再从数据库读取,读取后的数据再写到缓存;对于写操作,我们先写数据库,再写缓存
2、异步更新,应用层只读缓存,全量数据会保存在缓存里,并且不设置缓存过期的时间,由异步更新服务将数据库里变更的或新增的数据更新到缓存。也有通过MySQL的binlog将MySQL中的更新操作推送到缓存的实现方式
3、串联模式,应用直接在缓存上读写数据,缓存作为代理层,根据需要和数据库进行读写操作。
4.4.3、分部署缓存分片的三种模式
1、客户端分片,分片规则需要在同一个应用的多个节点保存,一般通过依赖jar包实现,如redic框架
2、代理分片,应用层和缓存层之间增加一个代理层,分片的路由规则写到代理层,如Codis框架
3、集群分片,Redis 3.0提供了集群
4.4.4、分布式缓存的迁移方案
1、平滑迁移,分为4个步骤:双写、迁移历史数据、切读、下双写
2、停机迁移,一般在晚上交易量比较小或者非核心服务的场景下比较适用
3、一致性hash(pedis框架)
— 求出Redis服务器的哈希值,并将其配置到0-2的32次方的圆上
— 采用同样方法将存储数据的键的hash值,并映射到相同的圆上
— 从数据库映射到的位置开始顺时针查找,找到的第一台服务器就是数据的保存位置
— 如果在寻找的过程中超过2的32次方仍然找不到节点,就会保存到第一台服务器上
4.4.5、缓存穿透、缓存并发和缓存雪崩
1、缓存穿透,指的是使用不存在的key进行大量的高并发查询,导致缓存无法命中,每个请求都穿透到后端数据库,压垮数据库(业务代码里捕获这种异常请求,将查询结果写入到缓存)
2、缓存并发,当一个缓存key过期时,因为访问这个缓存的请求较大,多个请求同时出现缓存过期,这样多个请求会同时访问数据库,并且回写到缓存,压垮数据库
解决办法:
— 分布式锁
— 本地锁,限制只有一个线程去数据库读
— 软过期,设置数据的缓存失效时间为不同的值
3、缓存雪崩,大量缓存集中在某一个时间内失效,请求压垮数据库
通常的解决办法是设置不同的过期时间
4.4.6、缓存对事务的支持
在服务器端可以利用服务器单线程LUA脚本来保证,或者通过watch,exec,discard,来保证
第五章 大数据利器之Elasticsearch
5.1、Lucene介绍
Lucent采用了基于倒排表的设计原理,可以非常高效地实现文本查找,在底层采用了分段的存储模式,使他在读写时几乎完全避免了锁的出现,大大提升了读写性能。
5.1.1、核心模块
— analysis模块:分词
— index模块:建索引
— store模块:负责索引读写
— queryParser:语法分析
— search模块:对索引的搜索
— similarity模块:负责相关性打分和排序的实现
5.1.2、核心术语
— Term:单词,索引里最小的存储和查询单元
— 词典:Term的集合,词典的数据结构有HashMap(性能高,浪费空间),fst(finite-state transducer)有更好的数据压缩和查询效率
— 倒排表:记录的是某个词在哪些文章出现过
— 正向信息:原始的文档信息,可以用来做排序、聚合、展示
— 段:索引中最小的存储单元,只读不能写
5.1.3、检索方式
1、单词查询
2、AND
3、OR
4、NOT
5.1.4、分段存储
段不变性的有点:
— 不需要锁
— 可以常驻内存
— 缓存友好
— 增量创建
段不变性的缺点:
— 删除时,旧数据不会立即被删除,而是标记为.del,等到段更新时才真正的删除
— 更新由删除和新增组成
— 由于索引具有不变性,每次新增数据时,都需要新增一个段来存储数据
— 查询出来的数据需要对已删除的数据进行过滤
为了提升写的性能,Lucene采用了延迟写的策略。先写内存,然后批量写入磁盘,若有一个段被写到磁盘,则生成一个提交点。
5.1.5、段合并策略
为了控制所有里段的数量,我们必须定期进行段的合并操作
5.1.6、Lucene相似度打分
1、文本相似度的主要影响因子
— tf:词频,其值越大,这篇文章描述的内容与该词越接近
— idf:(inverse document frequency),表示整个文档包含某个词的文档数量越少,这个便越重要
— length:同等条件下,搜索词所在的文档长度越长,搜索词和文档的相似度就越低;文档长度越短,相似度就越高
— term boost:查询在语句中每个词的权重
— document boost:文档权重
— field boost:域的权重,就是字段权重
— query boost:查询条件的权重
2、基于向量空间模型
向量空间模型的主要思路是把文本信息映射到空间向量中,形成文本信息和向量数据的映射关系,然后通过计算几个或者多个不同的向量的差异,来计算文本的相似度
3、基于概率的模型
BM25算法是根据BIM算法改进而来的,目前为止,是优秀的排名算法
5.2、Elastisearch简介
5.2.1、核心概念
— Cluster:集群
— Node
— Shards:分片
— Replicas:副本
— Index:索引
— Type:类别
— Document:文档
— Settings:集群中索引的定义
— Mapping,类似关系数据库的表结构信息
— Analyzer:字段的分词方式的定义
Elastic search的节点分类如下:
— 主节点,负责创建索引、删除索引、分配分片、追踪集群中的节点状态
— 数据节点,负责数据的存储和相关具体操作,如索引的创建、修改、删除、搜索、聚合
— 客户端节点,既不是候选主节点也不是数据节点的节点
— 部落节点,跨越多个集群
— 协调节点,任何节点都可以成为协调节点
集群的状态有红黄绿三种。
5.2.2、3C和脑裂
1、共识性(Consensus),分布式系统中所有节点必须对给定的数据或者节点的状态达成共识(zen discovery)
2、并发(Concurrency),
— 乐观并发控制(OCC),解决写-写冲突的无锁并发控制
— 多版本并发控制:使用版本号控制
3、一致性(Consistency)
三种允许写操作的判断
— One:只要主分片可用,就写
— All:只有当主分片和所有副本都可用时,才允许写操作
— Quorum:默认选项,一半以上节点可用就写
4、脑裂
集群中出现多个主节点时,可能会丢失数据,这种现象称为脑裂
5.2.3、事务日志
内存里的日志不可搜索,文件系统里的日志可以搜索
第六章 全面揭秘分布式定时任务
6.1、什么是定时任务
常用定时任务
1、crontab命令
Linux下的任务调度分为两类,系统任务调度和用户任务调度
— 系统任务调度:操作系统保存一个针对整个系统的crontab文件,该文件通常存放于/etc或者/etc的子目录下面
— 用户任务调度:每个用户都有自己的crontab文件,所有用户定义的文件都存放于/var/spool/cron目录中,文件名与用户名一致
crontab配置文件格式说明:
# * * * * * command
# | | | | | |
# | | | | | --要执行的命令
# | | | | —星期(0-7,星期日为0或7)
# | | | —月(1-12)
# | | —日(1-31)
# | —小时(0-23)
# —分钟(0-59)
— 以逗号隔开表示一个列表范围
— 连字符,指定值得范围
— 星号,代表任何的值
— 正斜线,指定时间的频率
— -u 设定某个用户的crontab文件
— file,命令文件的名称
— -e 编辑某个用户的crontab文件
— -l 用于显示某个用户的crontab文件
— -r 用于删除某个用户的crontab文件
— -i 用于在删除用户crontab文件时给出提示
2、JDKTimer
是一个单线程,定时调度所有TimerTask任务
Timer类的主要函数:Timer,schedule,然后重新run方法
Timer类的缺陷:
— 时间不准确延迟
— 异常终止
— 执行周期任务时依赖系统时间
3、ScheduleExecutor
每个调度任务都会有线程池中的一个线程去执行,因此任务是并发执行的。
ScheduleExecutor与Timer相比:
— 可以解决Timer不准时延迟的问题
— 多线程并发,能做到线程隔离
— 不依赖系统时间的变化而发生执行上的变化
— 执行过程中抛出异常,就会停止
4、Spring Schedule
主要包括TaskExecutor、TaskSchedule、Trigger三个抽象接口
5、Quartz,开源项目
框架的核心对象如下:
— Job,表示一个工作,为要执行的具体内容
— JobDetail,任务调度细节
— Trigger,执行任务的规则
— Schedule,代表一个任务调度容器
6、cron表达式
在线表达式工具(http://www.pppet.net)
6.2、分布式定时任务
6.2.1、定时任务使用场景
— 业务需求
— 产品运营
— 运维管理
6.2.2、传统定时任务存在的问题
1、只在一台服务器上运行
2、通过配置参数分散运行
3、通过全局锁互斥执行
6.2.3、分布式定时任务及其原理
分布式定时任务特点:
— 高可用性
— 可伸缩性
— 负载均衡
— 失效转移
分布式锁有3种实现方式
— 基于数据库的实现方式,依靠数据库的全局索引实现
— 基于Redis的实现方式
— 基于zookeeper的实现方式
1、永久节点
2、临时节点
3、顺序节点
6.3、开源分布式定时任务的用法
6.3.1、Quartz的分布式模式
6.3.2、TBSchedule(阿里巴巴开源的分布式调度框架)
6.3.3、Elastic-Job
当当网开源的分布式调度解决方案
第七章 RPC服务的发展历程和对比分析
7.1、什么是RPC服务
RPC协议是一种通过网络向远程计算机请求服务,而不需要了解底层网络技术的协议。
RPC有以下优势:
— 简单
— 高效
— 通用
7.2、RPC的服务原理
7.2.1、Socket套接字
双向通信
7.2.2、RPC的调用过程
step1 客户端调用本地的客户端存根方法
step2 客户端存根通过系统调用,使用操作系统内核套接字向远程服务发送编码的网络消息
step3 网络消息由内核通过TCP/UDP传输到远程服务器
step4 服务端存根接收客户端消息,并对消息进行解码
step5 服务端存根调用服务端方法,并将从客户端接收到的参数传递给该方法
step6 服务端执行完成后,把返回结果存入服务端存根代码中
step7 服务端存根将返回值编码并序列化后,通过一个或多个网络消息返回给客户端
step8 消息通过网络传输给客户端
step9 客户端从本地存根读取本地套接字消息
step10 客户端存根将消息返回给客户端函数,并将消息从网络二进制转换为本地语言格式
可伸缩服务架构:框架与中间件读书笔记:dubbo实现原理
2018-08-25 23:57:15 nullguo 阅读数 630 收藏 更多
前言:
在很久之前就对rpc(远程过程调用)框架非常感兴趣,然后主动去学习了阿里开源框架dubbo,最近阿里也是重启了dubbo的更新,dubbo又重新散发出活力。
当然学习首先是看dubbo官方文档,官方文档有非常详细的功能说明以及使用方法,对于使用dubbo的开发者已经足够了。最近在实习的公司中也有使用到dubbo来编写接口,我辛辛苦苦学习的dubbo框架终于有了用武之地。但我觉得还不够,今天在可伸缩服务架构:框架与中间件一书中看到有dubbo源码解析的一部分,大为惊喜!立刻打开博客开始记录起来。
源码分析:
dubbo初始化是通过spi,spi解释如下:
首先编写dubbo.xml时需要解析dubbo标签:<dubbo:/>,而解析dubbo标签需要以下步骤:
dubbo的核心接口Protocol:
dubbo的URL设计:
dubbo服务暴露过程:
引用服务过程:
FailOverCluster分析:
总结:
服务暴露原理:dubbo的namespacehandler里面会使用serviceconfig来解析dubbo标签获取ref,里面的afterpropertiesset方法将设置service配置以及暴露服务,利用proxyFactory(Javassist增强字节码,内部直接调用方法不是反射调用)的getInvoker方法将服务对象转化为invoker,利用Protocol的export方法将invoker转化为exporter(不同协议),然后放进exporters列表中。Serviceconfig最后监听到ContextRefreshed事件后开始暴露方法,利用netty去绑定端口监听服务调用。
调用服务原理:dubbo的namespacehandler里面会使用referenceconfig来解析dubbo标签获取interface,里面的afterpropertiesset方法将设置reference配置以及返回接口代理对象,利用Protocol的refer方法生成Invoker,利用ProxyFactory将Invoker转化为接口ref。生成接口ref时会根据配置信息产生对应的Cluster,将directory中的多个invoker整合成一个invoker对象。Cluster会根据容错机制调用select方法选择Invoker来调用产生结果返回,select方法里面带有配置信息中的loadbalance参数,select根据loadbalance来选择其中的Invoker返回给Cluster。