原文出处:wanglizhi
第一章 分布式系统介绍
分布式系统的定义:组件分布在网络计算机上,组件间仅仅通过消息传递来通信并协调行动。
分布式系统的意义:
- 升级单机处理能力的性价比越来越低
- 单机处理能力存在瓶颈
- 处于稳定性和可用性的考虑
摩尔定律:当价格不变时,每隔18个月,集成电路上可容纳的晶体管数目会增加一倍,性能也将提升一倍。
线程与进程的执行模式
冯诺依曼结构:输入设备、输入设备、运算器、控制器、存储器。
基于共享容器协同的多线程模式:经典如生产者消费者问题,对于存储数据的容器或对象,有线程安全和不安全之分,对于不安全的容器或对象,一般可以通过加锁或者通过Copy On Write的方式控制并发。
通过事件协同的多线程模式:避免死锁
多进程模式:
- 线程是属于进程的,一个进程内的多个线程共享了进程的内存空间;而多个进程间的内存空间是独立的,因此多个进程间通过内存共享、交换数据的方式与多个线程间就有所不同
- 此外,进程间通信、协调,以及通过一些事件通知或者等待一些互斥锁的释放方面也不一样
- 多进程相对于单进程多线程来说,资源控制会更容易实现;多进程中单个进程出现问题,不会造成整体的不可用
- 多进程之间可以共享数据,但其代价较大,会涉及序列化和反序列化的开销
网络通信基础知识
OSI七层模型与TCP/IP模型:
Socket套接字进行网络通信开发时,用到的三种方式:BIO、NIO和AIO
BIO:Blocking IO,采用阻塞的方式实现,一个线程处理一个Socket,发生建立连接、读数据、写数据的操作时,都可能会阻塞。
NIO:Nonblocking IO,基于时间驱动思想,采用Reactor模式,可以在一个线程中处理多个Socket套接字
AIO:AsynchronousIO,异步IO,采用Proactor模式,与NIO的差别是,AIO在进行读写操作时,只需要调用响应的read/write方法,并且需要传入CompletionHandler,在动作完成后会调用。
如何把应用从单机扩展到分布式
输入设备的变化
输出设备的变化
控制器的变化
方式1和2,透明代理:对发起方和处理方都是透明的
- 使用硬件负载均衡
- 使用LVS(或其他软件负载均衡系统)
缺点:
- 会增加网络的开销,一方面指流量,另一方面指延迟
- 这个透明代理处于请求的必经之路,如果代理出现问题,所有请求都会受到影响。我们需要考虑代理服务器的热备份
方式3,采用名称服务器直连的方式:
请求发起方和处理方直接没有代理服务器,而是直接连接。外部多了一个“名称服务”的角色,作用有:
- 收集提供请求处理的服务器的地址信息
- 提供这些地址信息给请求发起方
名称服务只是起到一个地址交换的作用,在发起请求的机器上,需要根据从名称服务得到的地址进行负载均衡的工作。
优点如下:
- 名称服务器出现问题,有办法可以保证处理正常
- 发起方和处理方直连,减少中间路径和带宽小号
缺点就是代码升级较复杂
方式4,采用规则服务器控制路由的请求直连调用
与名称服务器不同的是,规则服务器并不和请求处理的机器交互,只负责把规则提供给请求发起的机器。
方式5,Master+Worker的方式
存在一个Master节点来管理任务,由Master把任务分配给不同的Worker进行处理。
运算器的变化
- 通过DNS服务器进行调度和控制
- 增加负载均衡设备,DNS返回的永远是负载均衡地址
存储器的变化
同控制器的变化,加代理服务器、or名称服务器、or规则服务器
分布式系统的难点
- 缺乏全局时钟
- 面对故障独立性
- 处理单点故障,如果不能把单点变为集群,则需要给单点做好备份,降低单点故障影响范围
- 事务的挑战:2PC、最终一致、BASE、CAP、Paxos等
第二章 大型网站及其架构演进过程
大型网站:访问量(PV)、数据量、业务复杂度
单机负载告警,数据库与应用分离
应用服务器负载告警,走向集群
- 服务器选择问题:DNS、集群前加负载均衡设备
- Session的问题
Session保存会话状态,在Web服务器上,各个会话独立存储,多台服务器不能保证每次请求都落在同一边的服务器上。解决方案如下:
1、Session Sticky:负载均衡根据会话标识进行转发,让同样的Session请求每次都发送到同一个服务器端处理
缺点:
- 如果一台Web服务器宕机或重启,会话数据会丢失,用户要重新登录
- 会话标识是应用层信息,则负载均衡要在应用层进行解析,开销比在第四层大
- 负载均衡变为了有状态的节点,要将会话保存到具体Web服务器的映射。内存消耗会变大,容灾更麻烦
2、Session Replication:会话在多态服务器上复制同步
缺点:
- 同步Session数据造成了网络带宽的开销
- 每台Web服务器都要保存所有的Session数据,数据量容易很大
3、Session数据集中存储
Session数据不再Web服务器上,而是放在另一个集中存储的地方。
缺点:
- 读写Session数据引入了网络操作,存在时延和不稳定性
- 如果集中存储Session的机器或者集群有问题,就会影响我们的应用
4、Cookie Based:把Session数据放在Cookie中
缺点:
- Cookie长度的限制
- 安全性:外部访问和修改
- 带宽消耗
- 性能影响:每次HTTP请求都带有Session数据
数据读压力变大,读写分离
1、采用数据库作为读库
缺点:
- 数据复制问题;
- 应用对于数据源的选择问题:写操作和事务走主库,考虑从库相对主库的延迟
2、搜索引擎其实是一个读库
3、加速数据读取的利器——缓存
- 数据缓存,Key-Value,“热数据”,容量不够时清除缓存
- 页面缓存,ESI标签页面缓存
弥补关系型数据库的不足,引入分布式存储系统
分布式文件系统,解决小文件和大文件的存储问题 分布式key-value系统,提供高性能的半结构化支持 分布式数据库提供一个支持大数据、高并发的数据库系统
读写分离后,数据库又遇到瓶颈
尽管读写分离以及分布式存储系统,能够降低主库的压力,但是交易、商品、用户的数据都还在一个数据库中,压力还在继续增加,我们有数据垂直拆分和水平拆分两种选择;
1、专库专用,数据垂直拆分
垂直拆分即把不同的业务数据分到不同的数据库中。
问题:
- 应用需要多个数据源,带来的是每个数据库连接池的隔离
- 单机跨业务事务,一种方法是使用分布式事务,性能较低;另一种办法就是去掉事务
2、单表达到瓶颈,数据水平拆分
水平拆分就是把同一个表的数据拆到两个数据库中。
问题:
- SQL路由问题,选择哪个数据表
- 主键处理等机制不同,如自增主键
- 一些查询需要从两个数据库中取数据,加上分页操作,比较难处理
数据库问题解决后,应用面对的新挑战
拆分应用
- 根据业务特性,还可以根据用户注册、登陆、用户信息维护等再拆分。
- 走服务化的路,共享代码放在各个服务中心,如商品中心、用户中心、交易中心
初识消息中间件
消息中间件是在分布式系统中完成消息发送和接收的基础软件。两个明显好处:异步、解耦。
第三章 构建Java中间件
三个领域的中间件:
- 远程过程调用和对象访问中间件:主要解决分布式环境下应用的互相访问问题。是支撑应用服务化的基础
- 消息中间件:解决应用之间的消息传递、解耦、异步的问题
- 数据访问中间件:解决应用访问数据库的共性问题
构建Java中间件的基础知识
JVM中堆分为三块:Young/Tenured/Perm,新生代/年老代/持久代
一般来说,新对象分配在新生代的Eden区,也可能直接分配在年老代,在进行新生代垃圾回收时,Eden区存活的对象被复制到空的Survivor区,在下次新生代回收时,Eden区存活的对象和这个Survivor存活的对象被复制到另外那个Survivor区,并且清空当前Survivor区,经过多次新生代垃圾回收,还存活的对象会被移动到年老代。
线程池
1 2 3 4 | ThreadPoolExecutor tp = new ThreadPoolExecutor( 1 , 1 , 60 , TimeUnit, SECONDS, new LinkedBlockingQueue<Runnable>(count)); tp.execute( new Runnable(){ public void run(){} }); |
使用线程池的方式是复用线程的,不用每次都创建线程。而创建线程的开销占比较大。
synchronized
synchronized修饰静态方法、对象方法、代码块
ReetrantLock
- **提供tryLock方法,尝试调用,如果锁被其他线程持有,则tryLock立即返回
- 可以实现公平锁
- ReentrantReadWriteLock:读写锁,用于读多写少并且读不需要互斥的场景
- 可以有多个Condition
volatile
可见性指一个线程修改变量值后,其他线程中能够看到这个值。volatile虽然解决了可见性问题,但是不能控制并发
Atomics
原子操作,如AtomicInteger内部通过JNI的方式使用了硬件支持的CAS指令
wait、notify和notifyAll
wait是等待线程,notify是唤醒一个等待线程(并不能指定,随机),notifyAll是唤醒所有的等待线程。
CountDownLatch
java.util.concurrent包中的一个类,主要提供的机制是当多个线程都到达了预期状态或完成预期工作时触发事件,其他线程可以等待这个事件来触发自己后续的工作。
CyclicBarrier
循环屏障,可以协同多个线程,让多个线程在这个屏障前等待,知道所有线程都到达了这个屏障时,再一起继续执行后面的动作。
Semaphore
Semaphore是用于管理信号量的,构造时传入可供管理的信号量的数值。如果信号量只有一个,就退化到互斥锁了,如果多于一个,则主要用于控制并发数。
Exchanger
用于两个线程之间进行数据交换,线程会阻塞在exchange方法上,知道另外一个线程也到了同一个Exchanger的exchange方法时,二者进行交换。
Future和FutureTask
Future是一个接口,FutureTask是一个具体实现类
1 2 3 4 5 6 7 8 9 10 11 | Future<HashMap> future = getDataFromRemote2(); ...... HashMap data = (HashMap) future.get(); public Future<HashMap> getDataFromRemote2(){ return threadPool.submit( new Callable<HashMap>(){ public HashMap call() throws Exception{ return getDataFromRemote(); } }); } |
getDataFromRemote2还是使用率getDataFromRemote完成操作,并且用到了线程池:把任务加入线程池中,把Future对象返回出去。
并发容器
- CopyOnWrite:更改容器时,把容器复制一份进行修改,用于读多写少
- Concurrent:尽量保证读不加锁,并且修改时不影响读,所以比读写锁更高的并发性能
动态代理
继承InvocationHandler
反射
Java反射机制是指在运行状态,对于任意一个类,都能知道这个类所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性。
1 2 3 4 5 6 7 8 9 10 11 12 | Class clazz = Object.getClass(); String className = clazz.getName(); Method[] methods = clazz.getDeclaredMethods(); Field[] fields = clazz.getDeclaredFields(); // 构建对象 Class.forName( "ClassName" ).newInstance(); // 动态执行方法 Method method = clazz.getDeclaredMethod( "add" , int . class , int . class ); method.invoke( this , 1 , 1 ); // 动态操作方法 Field field = clazz.getDeclaredField( "name" ); field.set( this , "test" ); |
网络通信的选择
BIO、NIO、AIO
第三方框架,MINA,Netty
第四章 服务框架
服务调用端的设计与实现
调用发起==>寻址路由==>协议适配和序列化==>网络传输
==>反序列化 协议解析==>得到结果返回给调用方
1、确定服务框架的使用方式
2、服务调用者与服务提供者之间通信方式的选择
3、引入基于接口、方法、参数的路由
4、多机房场景,避免跨机房调用,一是在服务注册中心甄别,二是地址过滤
5、服务调用端的流控处理
6、序列化与反序列化处理,Java本身的序列化性能问题、跨语言问题、序列化后语言长度等
7、网络通信实现选择:BIO、NIO、AIO
8、支持多种异步服务调用方式:Oneway,Callback,Future,可靠异步
服务提供端的设计与实现
1、如何暴露远程服务
2、服务端对请求处理的流程
3、执行不同服务的线程池隔离
4、服务提供端的流控处理
第五章 数据访问层
分布式事务
- 两阶段提交:2PC
- 一致性理论:CAP、BASE
- Paxos协议
多机自增主键问题
考虑唯一性和连续性,UUID生成方式(IP、MAC、时间等)连续性不好
实现方案1:把ID集中放在一个地方进行管理,对每个Id序列独立管理,每台机器使用Id时都从这个Id生成器上取。
缺点:
- 性能问题:每次都去远程取Id会有资源损耗
- 生成器的稳定性问题,作为一个无状态的集群,保证可用性
- 存储的问题
实现方案2:舍掉Id生成器,把相关的逻辑放到需要生成Id的应用本身。每个生成器读取可用的Id,然后给应用使用,但是数据的Id并不是严格按照进入数据库顺序而增大的。
应对多机的数据查询
跨库Join
- 在应用层把原来数据库的Join操作分成多次的数据库操作
- 数据冗余,对常用信息进行冗余
- 借助外部系统,如搜索引擎
外键约束
外键约束比较难解决,不能完全依赖数据库本身来完成之前的功能了。
跨库查询的问题及解决
一张逻辑表,对应多个数据库的多张数据表,在一些场景下比较复杂,如排序、最大最小求和等函数处理、求平均值、非排序分页、排序后分页。
如何对外提供数据访问层的功能
1、为用户提供专有API
2、通用的方式,数据层JDBC
3、基于ORM或类ORM接口的方式
直接基于JDBC驱动方式较好~
数据层的整体流程
SQL解析==>规则处理==>SQL改写==>数据源选择==>SQL执行==>结果集返回合并处理
1、SQL解析阶段
- SQL解析并不完备
- SQL中不带有分库条件,但实际上是可以明确指定分库的
2、规则处理阶段
- 采用固定哈希算法作为规则,如根据用户id取模,id mod 2分库,再id mod 4分表。实现简单,但是如果扩容的话比较复杂!
- 一致性哈希,节点对应的哈希值为一个范围,分配给现有节点。如果有节点加入,会从原有节点分管一部分范围的哈希值;如果有节点退出,会把哈希值交给下一个节点管理
- 虚拟节点对一致性哈希的改进,引入虚拟节点,如4个物理节点可以变为多个虚拟节点,每个虚拟节点支持连续的哈希环上的一段。
- 映射表与规则自定义计算方式,映射表是根据分库分表字段的值的查表法来确定数据源的方法,一般用于对热点数据的特殊处理。
3、为什么要改写SQL
分库分表后,同一个卖家的商品可能会分在多个库中,查询就要跨库。分布的不同数据库中的表的结构虽然一样,但是表的名字、索引名字未必一样,所以要修改SQL。
还有需要修改SQL的地方,如跨库计算平均值,必须修改SQL获取数量、总数后再进行计算。
4、如何选择数据源,读写分析
5、执行SQL和结果处理阶段,异常处理和判断
第六章 消息中间件
JMS,Java Message Service是Java EE中关于消息的规范,ActiveMQ等是对这个规范的实现。如果是小型系统直接使用JMS是一个经济的选择,在大型系统中不适合使用JMS。
如何解决消息发送一致性
消息发送一致性是指产生消息的业务动作与消息发送一致,即如果业务操作成功了,那么由这个操作产生的消息一定要发送出去。
1、发送消息给消息中间件
2、消息中间件入库消息
3、消息中间件返回结果
4、业务操作
5、发送业务操作结果给消息中间件
6、更改存储中消息状态
……
注:后面内容略,不方便摘要
参考:《大型网站系统与Java中间件实践》