分布式系统的基础知识
- 阿姆达尔定律
- 多线程交互模式
- 互不通信,没有交集,各自执行各自的任务和逻辑
- 基于共享容器(如队列)协同的多线程模式->生产者-消费者->队列
- 通过事件协同的多线程模式->如B线程需要等到某个状态或事件发生后才能继续工作,而这个状态改变或者事件产生和A线程相关
- 避免死锁
- 网络通信基础知识
- OSI、TCP/IP
- 网络IO实现方式
- BIO
- NIO->Reactor模式
- AIO->Proactor模式
负载均衡
- 硬件负载均衡
- LVS等软件的负载均衡
- 名称服务
- 规则服务器
- Master-Worker
小结
1.一致性hash的算法思路(游戏服务器进程/线程设计思路)(扩容/缩容平滑)/环形消息队列disruptor
2.缓存服务器扩容或者缩容要尽量平滑
3.消息中间件-MOM:Message-Oriented middleware,is software infrastructure focused on sending and receiving messages between distributed system.
4.两个常被提及的好处:异步和解耦.
5.CountDownLatch不能循环使用,而CyclicBarrier可以循环使用(从名字上看,【循环_cyclic】屏障)
大型网站及其架构演进过程
海量数据+高并发访问量+复杂的业务和系统
- 数据库与应用分离
- 应用服务器走向集群(引入负载均衡器)
session问题
- Session Sticky
- Session Replication
- Session数据集中存储
- Cookie Based
读写分离
- 引入读库->搜索引擎(站内搜索功能)->Search Cluster
- 专库专用->数据垂直拆分
- 数据水平拆分
加快数据读取
- 缓存->页面缓存->Cache Cluster
分布式存储系统
- Distributed Storage
拆分应用
- 走服务化的路
消息中间件
中间件
特定中间件是解决特定场景问题的组件,它能够让软件开发人员专注于自己应用的开发
- 远程过程调用和对象访问中间件->解决分布式环境下应用的互相访问问题
- 消息中间件:解决应用之间的消息传递、解耦、异步的问题
- 数据访问中间件:解决应用访问数据库的共性问题的组件
构建Java中间件的基础知识
- JVM/GC/内存堆布局
- 并发
- 线程池/synchronized/ReentrantLock/volatile(可见性与操作互斥是两件事情)/Atomics/wait、nofity、nofifyAll/CountDownLatch/
- CyclicBarrier/Semaphore/Exchanger/Future、FutureTask/
- 并发容器:CopyOnWrite、Concurrent
- 动态代理
- 反射
- 网络通信选择
- BIO/NIO/AIO
- MINA、Netty
- 协议制定
中间件范畴
- 应用的拆分
- 服务的拆分
- 数据的拆分
- 应用的解耦
服务框架
运行期服务框架与应用和容器的关系
- 直接或者间接依赖的jar导致应用里同一个jar包有不同的版本->产生冲突
- 将服务框架自身用到的类和应用用到的类都控制在User-Defined Class Loader级别->实现相互间隔离
- web容器对于多个web应用的处理以及osgi对于不同的bundle的处理都采用了类似的方法
服务调用者与服务提供者之间通讯方式的选择
- 调用者[服务框架]
- -->服务注册查找中心
- --->服务提供者[服务框架]
- -->服务注册查找中心
- 并不是每次调用远程服务前都通过服务注册查找中心来查找可用地址->而是把地址缓存在调用者本地->当有变化时主动从服务注册查找中心发起通知->告诉调用者可用的服务提供者列表的变化
- 客户端拿到可用的服务提供者的地址列表后->如何为当次的调用进行选择就是路由要解决的问题->随机、轮训、权重(一般指动态权重)
引入基于接口、方法、参数的路由
- 因为一般情况下,一个集群会提供多个服务,每个服务又有多个方法.->需要细粒度的控制服务路由
- 如服务提供者有两个服务-接口A和接口B->接口A中的某个方法1执行比较耗时->影响整体性能->排队
- 隔离资源,使得快慢不同、重要级别不同的方法之间互不影响
服务调用端的流控处理
- 控制到服务提供者的请求的流量
序列化与反序列化处理
- 具*定通信协议时->版本号、可扩展(扩展性、向后兼容性)属性以及发起方支持能力的介绍(如是否支持压缩等)
网络通信实现选择
- 同步方式进行远程调用使用nio
- io线程/数据队列/通信对象队列/定时任务
- 请求线程发送数据->进入数据队列->生成通信对象(阻塞请求线程)->加入通信对象队列->请求线程等待(通信对象用于唤醒请求线程)
- 数据队列->io线程->socket连接->进行数据收发(需要发送的数据进入数据队列后,这样请求线程就不需要直接和socket连接打交道->复用socket连接)
- 如果远程调用超时前有执行结果返回->io线程会通知通信对象->通信对象结束请求线程等待->结果传送给请求线程
- 定时任务用于负责检查通信对象队列中的哪些通信对象已经超时->然后这些通信对象会通知请求线程已经超时
- 支持多种异步服务调用方式
- OneWay->只管发送请求而不关心结果的方式->只需要把发送的数据放入数据队列即可
- CallBack->请求发送方发送请求后会继续执行自己的操作->等对方有响应时进行一个回调
- 请求方设置回调->加入回调对象队列
- 收到服务提供者的返回后->io线程会通知回调对象->执行回调方法
- 对于超时->定时任务的方式->如果没有返回->也需要执行回调对象的方法->告知已超时没有结果
- 如果不引入新的线程->那么回调的执行要么是io线程中要么是在定时任务的线程中
- [建议用新的线程执行回调->不要因为回调本身的代码执行时间久等问题影响了io线程或者定时任务].
- Future->请求线程通过future来获取通信结果并直接控制超时->同上Future对象队列 io线程依然是从数据队列中得到数据再进行通信->得到结果后会把它传给Future
- 可靠异步->要保证异步请求能够在远程被执行->消息中间件来完成这个保证
- 使用Future方式对远程服务调用的优化
- 一个请求中调用多个远程服务的情况
- 按照调用顺序把服务的请求发送给服务A,服务B,服务C->请求发送过去并不直接等待执行结果
- 而是直到服务C的请求也发出去后再来统一等待服务A、服务B和服务C的执行结果->然后再接着进行本地的数据处理
- 前提:所调用服务A、B、C之间并没有相互的依赖关系
- 即并行调用优化->因为Future方式的支持
- 反序列化线程:一般是使用io线程,不过这样会影响io线程的工作效率;另一种方式是把反序列化工作从io线程转移到其他线程去做
服务提供端的设计与实现
- 服务端的工作
- 本地服务的注册管理
- 根据进来的请求定位服务并执行
- IO线程通信处理->反序列化的工作取决于具体实现,io线程或者工作线程中进行的方式都有
- 得到反序列化的消息并定位服务后->调用服务一般在非io线程进行(工作线程)
- 执行不同服务的线程池隔离
- 服务提供端,工作线程池不止一个,而是多个,定位到服务后,根据服务名称、方法、参数来确定具体执行服务调用的是哪个线程池.->这样,不同线程池之间是隔离的->不会出现争夺线程资源的情况.
- 整个服务框架的功能分为服务调用者和服务提供者两方面,此外像序列化、协议、通信等是公用的功能.在具体实现上,是把这些功能都放在一起形成一个完成的服务框架->而不是分为服务调用者框架和服务提供者框架
- 服务框架必须做到模块化且可配置->模块可替换->并留有一定的扩展点来扩展原有功能
服务升级
- 接口不变->内部的服务实现有变化->比较简单,采用灰度发布的方式验证然后全部发布就可以了
- 接口中增加方法->也比较简单,直接增加方法即可->需要使用新方法的调用者就使用新方法,原来的调用者继续使用原来的方法即可
- 接口的某些方法修改调用的参数列表
- 对使用原来方法的代码都进行修改->不太可行->因为要求我们同时发布多个系统
- 版本号解决->常用的方式->使用老方法的系统继续调用原来版本的服务,而需要使用新方法的系统则使用新版本的服务
- 在设计方法上考虑参数的扩展性->可行,不太好->因参数列表可扩展一般就以为是采用类似map的方式来传递参数->不直观,并且对参数的校验会比较复杂
实战中的优化
- 服务的拆分
- 要拆分的服务是需要为多方提供公共功能
- 服务的粒度
- 需要根据业务的实际情况来划分服务
- 优雅和实用的平衡
- 多调用一次就比之前多了一次网络->一些功能直接在服务调用者的机器上实现会更加合适、经济
- 如服务调用者直接读缓存->大部分对数据的请求直接走一次缓存就可以,只有少部分没有命中缓存的数据读取需要走服务提供者->然后再到数据库进行读取并插入缓存
- 分布式环境的请求合并
- 分布式锁-->额外开销->另外一个思路->在服务调用端不是把请求随机分发给服务提供者,而是根据一定的规则把同样的请求发送到同一个服务提供者上->减少复杂性.
服务治理
- 服务信息、服务质量、服务容量、服务依赖、服务分布、服务统计、服务元数据、服务查询、服务报表、服务监视、服务上下线、服务路由、服务限流降级、服务归组、服务线程池管理、机房规则、服务授权、
- ESB
- Enterprise Service Bus:企业服务总线
数据访问层
数据库减压思路
- 优化应用,看看是否有不必要的压力给了数据库(应用优化)
- 看看有没有办法可以降低对数据库的压力,例如引入缓存、加搜索引擎等
- 把数据库的数据和访问分到多台数据库上,分开支持->核心思路和逻辑
- 数据拆分->垂直拆分->水平拆分
单机变为多机后,事务如何处理
- 分布式事务->XA/DTP-->AP(Application Program)->RM(Resource Manager)->TM(Transaction Manager)
- 两阶段提交->Prepare->Commit
- Prepare阶段有一个节点资源失败则Rollback
- 大型网站一致性的基础理论-CAP/BASE
- CAP:Consitency,一致性、Availability,可用行、Partition-Tolerance,分区容忍性
- 分布式系统中不能同时满足上面三项,CA、AP、CP
- 分布式系统中,我们一般选择加强可用性和分区容忍性而牺牲一致性->首先先满足A和P,然后看如何解决C的问题.
- BASE模型
- Basically Available,基本可用,允许分区失败
- Soft state,软状态,接受一段时间的状态不同步
- Eventually consistent,最终一致,保证最终数据的状态是一致的
- 对于C,我们采用的方式和策略就是保持最终一致,也就是不保证数据发生变化后所有节点立刻一致,但是保证他们最终是一致的。在大型网站中,为了更好的保持扩展性和可用性,一般都不会选择强一致性,而是采用最终一致的策略来实现.
- 比两阶段提交更轻量一些的Paxos协议
- 核心原则:少数服从多数
- 集群内数据一致性的算法实例
- Quorum、Vector Clock算法
- 从工程上说,如果能够避免分布式事务的引入,那么还是避免为好;如果一定要引入分布式事务,那么可以考虑最终一致的方法,而不是追求强一致。而且从实现上来说,我们是通过补偿的机制不断重试,让之前因为异常而没有进行到底的操作继续进行而不是回滚。如果还是不能满足需求,那么基于Paxos的算法会是一个不错的选择.
多机的Sequence问题与处理
- 唯一性
- 连续性
- 提供一个实现方案:把所有id集中放到一个地方进行管理,每台机器使用时都从这个id生成器上取
跨库查询
数据访问层的设计与实现
- 对外提供数据访问层的方式
- 为用户提供专有API->不推荐->没有通用性
- 通用方式->jdbc->数据层自身可以作为一个jdbc的实现,即暴露出jdbc的接口给应用
- 基于orm或者类orm接口的方式
- 按照数据层流程的顺序看数据层设计
- SQL解析
- 通过SQL解析可以得到SQL中的关键信息,如表明、字段、where条件等;而在数据层中,一个很重要的事情是根据执行的SQL得到被操作的表,根据参数及规则来确定目标数据库连接
- 规则处理阶段
- 用固定哈希算法作为规则_分库分表
- 一致性 hash
- [把节点对应的哈希值变为了一个范围],而不再是离散的.在一致性哈希中,我们会把整个哈希值的范围定义的非常大,然后把这个范围分配给现有的节点
- 虚拟节点对一致性hash的改进
- 解决增加或减少节点时负载不均衡的问题
- 映射表与规则自定义方式
- 通过比较复杂的函数计算来解决数据访问的规则问题
- 为什么要改写SQL
- 多库多表->修改表名->跨库计算平均值等
- 如何选择数据源
- Master/Slave
- 根据当前SQL的特点(读、写)、是否在事务中以及各个库的权重规则,计算得到这次SQL请求要访问的数据库
- 执行SQL和结果处理阶段
- 对异常的处理和判断
- SQL解析
- 实战
- 复杂的连接管理
- 三层数据源的支持和选择
- DataSouce/AtomDataSource/groupDataSource
- 独立部署的数据访问层实现方式
- jar包方式
- proxy方式
- 数据库协议
- 私有协议
- 读写分离的挑战和应对
- 数据结果相同,多从库对应一主库的场景
- 应用通过数据层访问数据库,通过消息系统就数据库的更新送出消息通知->数据库同步服务器获得消息通知后会进行数据的复制工作.分库规则配置则负责在读数据及数据同步服务器更新分库时让数据层知道分库规则->数据同步服务器和DB主库的交互主要是根据被修改或新增的数据主键来获取内容,采用的是行复制的形式
- 比较优雅的方式是基于数据库的日志来进行数据的复制
- 主/备库分库方式不同的数据复制
- 非对称复制->控制数据分发->如主库按照买家id分库,而备库按照卖家id进行分库
- 引入数据变更平台
- 很多其他场景也会关心数据的变更,除了复制到其他数据库,例如缓存的失效等->可以考虑构建一个通用的平台来管理和控制数据变更->
- 引入Extractor和Applier->Extractor负责把数据源变更的信息加入到数据分发平台中,而Applier的作用是把这些变更应用到相应的目标上->中间的数据分发平台中是由多个管道组成->进入到数据分发平台的变更信息就是标准化、结构化的数据了-
- 如何做到数据平滑迁移
- 最大挑战是,在迁移的过程中又会有数据的变化(因为很多应用不能接受长时间的停机)->可以考虑的方案是在开始进行数据迁移时记录增量的日志,在迁移结束后,再对增量的变化进行处理.->在最后,可以要把要被迁移的数据的写暂停,保证增量日志都处理完毕后,再切换规则,放开所有的写,完成迁移工作
- 数据结果相同,多从库对应一主库的场景
消息中间件
消息中间件对应用的解耦
- 如登陆系统负责向消息中间件发送消息,而其他的系统则向消息中间件来订阅这个消息,然后完成自己的工作.
- 通过消息中间件解耦,登陆系统就不用关心到底有多少个系统需要知晓登陆成功这件事了,而不用关心如何通知它们,只需要把登陆成功这件事转化为一个消息发送到消息中间件就可以了
- landon:和事件解耦一样,如游戏中玩家升级抛出一个事件,其他子系统只需要监听该事件即可,而不必升级直接调用各个子系统
- 登陆成功时需要向消息中间件发送一个消息,那么[必须保证这个消息发送到了消息中间件],否则依赖这个消息的系统就无法工作了
互联网时代的消息中间件
- JMS:Java Message Service->规范->Hornetq,ActiveMQ等产品是这个规范的实现
- 如何解决消息发送一致性
- 消息发送一致性的定义:产生消息的业务动作与消息发送的一致,即如果业务操作成功了,那么由这个操作产生的消息一定要发送出去,否则就丢失消息了;而另一方面,如果这个业务行为没有发生或者失败,那么就不应该把消息发出去.
- JMS消息模型-Queue/Topic_支持XA协议(两阶段提交)->会引入分布式事务->存在一些限制且成本相对较高
- 一致性方案的正向流程
- (1) 业务处理应用首先把消息发给消息中间件,标记消息的状态为待处理.
- (2) 消息中间件收到消息后,把消息存储在消息存储中,并不投递该消息.
- (3)消息中间件返回消息处理的结果,仅是入库的结果,结果是成功或者失败.
- (4)业务方收到消息中间件返回的结果并进行处理:
- a) 如果收到的结果是失败,那么就放弃业务处理,结束
- b) 如果收到的结果是成功,则进行业务自身的操作
- (5)业务操作完成,把业务操作的结果发送给消息中间件
- (6)消息中间件收到业务操作结果,根据结果进行处理
- a) 如果业务失败,则删除消息存储中的消息,结束
- b)如果业务成功,则更新消息存储中的消息状态为可发送,并且进行调度,进行消息的投递
- 需要注意各种步骤中可能出现的异常情况
- 最终一致性方案的补偿流程:
- (1)消息中间件询问状态为待处理的消息对应业务操作结果
- (2)应用即消息发布者对业务操作检查操作结果
- (3)发送业务处理结果给消息中间件
- 4)消息中间件更新消息状态,业务成功,消息状态为待发送;业务失败则消息删除
- 如何解决消息中间件与使用者的强依赖问题
- 把消息中间件所需要的消息表与业务数据表放到同一个业务数据库->业务操作和写入消息作为一个本地事务完成,然后再通知消息中间件有消息可以发送->解决一致性->也可以消息中间件定时去轮询业务数据库找到需要发送的消息,取出内容后进行发送
- 需要业务自己的数据库承载消息数据/需要让消息中间件去访问业务数据库/需要业务操作的对象是一个数据库
- 消息中间件不再直接与业务数据库打交道->将业务操作、写入消息,轮询消息等全部放到业务应用
- 加一个本地磁盘作为一个消息存储
- 消息模型对消息接收的影响
- JMS Queue模型:
- 应用1和应用2发送消息到JMS服务器,这些消息根据到达的顺序形成一个队列->应用3和应用4进行消息的消费;如果Queue里面的消息被一个应用处理了,那么连接到JMS Queue上的另一个应用是收不到这个消息的->即连接到这个JMS Queue上的应用共同消费了所有的消息->消息从发送端发送出来时不能确定最终会被哪个应用消费,但是可以明确的是只有一个应用会去消费这条消息->Peer To Peer方式(PTP)
- JMS Topic模型:
- 和Queue模型的最大区别在于消息接收的部分,在该模型中,接收消息的应用3和应用4是可以独立收到所有到达Topic的消息的->Pub/Sub方式
- JMS中客户端连接的处理和带来的限制
- JMS中每个Connection都有一个唯一的clientId,用于标识连接的唯一性
- 应用3和JMS服务器建立了两个连接,应用4和JMS服务器建立了一个连接->可以看到这三个连接所接收的消息是完全不同,每个连接收到的消息条数以及收到消息的顺序则不是固定的.->另外每个连接都会收到所有发送到Topic的消息.
- 我们需要什么样的消息模型
- 消息发送方和接收方都是集群/同一个消息的接收方可能有多个集群进行消息的处理/不同集群对于同一条消息的处理不能相互干扰
- 如8条消息和两个集群,每个集群恰好有两台机器->那么需要这两个集群的机器分别处理掉所有8条消息->不能遗漏也不能重复
- 引入ClusterId,用这个Id来标识不同的集群,而集群内的各个应用实例的连接使用同样的ClusterId->把Topic模型和Queue模型的特点结合起来使用
- JMS Queue模型:
- 消息订阅者订阅消息的方式
- 作为消息中间件,提供对于消息的可靠保证是非常重要的事情->一些场景中一些下游系统完全通过消息中间件进行自身任务的驱动
- 持久订阅、非持久订阅
- 非持久订阅:消息接收者应用启动时,就建立了订阅关系->可以收到消息->如果消息接收者应用结束了,那么消息订阅关系也就不存在了->这时的消息是不会为消息接收者保留的.
- 持久订阅:消息订阅关系一旦建立除非应用显示地取消订阅关系否则这个订阅关系将一直存在即使消息接收者应用停止->这个消息也会保留,等待下次应用启动后再投递给消息接收者.
- 保证消息可靠性
- 消息从发送端应用到接收端应用,中间有三个阶段需要保证可靠,分别是:[消息发送者把消息发送到消息中间件];[消息中间件把消息存入消息存储];[消息中间件把消息投递给消息接收者]
- 要保证这三个阶段都可靠,才能保证最终消息的可靠
- 消息发送端可靠的保证->注意异对异常的处理->可能出现的问题是在不注意的情况下吃掉了异常->从而导致错误的判断结果
- 消息存储的可靠性保证
- 持久存储部分的代码完全自主实现
- 利用现有的存储系统实现
- 实现基于文件的消息存储
- 采用数据库作为消息存储
- 基于双机内存的消息存储
- 消息中间件自身扩容
- 让消息的发送者和消息的订阅者能够感知到有新的消息中间件机器加入到了机器->软负载中心
- 消息存储的扩容处理
- 服务端主动调度安排投递
- 消息投递的可靠性保证
- 消息接收者在处理消息的过程中对于异常的处理->千万不要吃掉异常后确认消息处理成功
- 投递处理优化:
- 投递是一定要采用多线程处理
- 单机多订阅者共享连接->消息只发送一次
- 订阅者视角的消息重复的产生和应对
- 分布式事务,复杂
- 幂等操作->对于消息接收端->采用同样的输入多次调用处理函数会得到同样的结果
- JMS的消息确认方式与消息重复的关系
- AUTOACKNOWLEDGE/CLIENTACKNOWLEDGE/DUPSOKACKNOWLEDGE
- 消息投递的其他属性支持
- 消息优先级
- 订阅者消息处理顺序和分级订阅
- 自定义属性
- 局部顺序
- 保证顺序的消息队列设计
- 接收端的设计从原来的Push模式变为了Pull模式
软负载中心与集中配置管理
- 软负载中心两个最基础的职责
- 聚合地址信息
- 生命周期感知->需要能对服务的上下线自动感知,并且根据这个变化去更新服务地址数据
- 软负载中心的结构
- 软负载中心的服务端->负责感知提供服务的机器是否在线,聚合提供者的机器信息并负责把数据传给使用数据的应用
- 软负载中心的客户端
- 服务提供者->把服务器提供者提供服务的具体信息主动传给服务端->并且随着提供服务的变化去更新数据
- 服务器使用者->向服务端告知自己所需要的数据并负责去更新数据,还要进行本地的数据缓存
- 软负载中心三部分重要的数据->聚合数据、订阅关系、连接数据
- 内容聚合功能的设计
- 保证数据正确性
- 高效聚合数据
- 并发下的数据正确性的保证
- 数据更新、删除的[顺序]保证
- 大量数据同时插入、更新时的性能保证
- 根据key进行分线程的处理->保证同样key的数据是在同一个线程中处理->顺序任务队列
- 解决服务上下线的感知
- 通过客户端与服务端的连接感知
- 长连接的心跳或数据的发布来判断服务发布者是否还在线->如果很久没有心跳或数据的发布,则判定为不在线;那么就取出这个发布者发布的数据->而对于新上线的发布者,通过连接建立和数据发布就实现了上线的通知
- 当负载中心的自身的负载很高时,可能产生误判,如软负载中心压力很大,处理请求变慢,心跳数据来不及处理->会以为心跳超时而判断服务不在线,认为服务不可用并且把信息通知给服务调用者,这会导致原本可用的服务被下线了
- 另外的问题,如果服务发布者到软负载中心的网络链路有问题而服务发布者到服务使用者的链路没问题,也会造成感知的问题->因为软负载中心属于旁路
- 解决:软负载中心客户端增加逻辑,当收到软负载中心通知的应用下线数据时,需要服务调用者进行验证才能接收这个通知
- 通过对于发布数据中提供的地址端口进行连接的检查
- 需要服务调用者进行最终确认,因为在系统中进行的实际业务调用通信是在服务调用者和服务提供者之间
- 通过客户端与服务端的连接感知
- 软负载中心的数据分发的特点和设计
- 数据分发与消息订阅的区别
- 消息中间件需要保证消息不丢失->每条消息都应该送到相关订阅者->而软负载中心只需要保证最新数据送到相关的订阅者->不需要保证每次的数据变化都能让最终订阅者感知
- 消息中间件中同一个集群中的不同机器是分享所有消息的,因为该消息只要同一集群中的一台机器去处理了就行->而软负载中心则不同,因为其维护的是大家都需要用的服务数据->所以需要把这数据分发给所有的机器
- 提升数据分发性能需要注意的问题
- 数据压缩->CPU换带宽
- 全量与增量的选择->建议刚开始的实现中采用简单的方式,即传送全量数据,当全量数据很大时就需要考虑采用增量传送的方式实现.
- 针对服务化的特性支持
- 软负载数据分组
- 根据环境进行划分
- 分优先级的隔离
- 提供自动感知以外的上下线开关
- 优雅的停止应用
- 我们应该先从服务列表中去掉这个机器->等待当时正在执行的服务器结束,然后再停止应用->通过指令直接从软负载中心使机器下线
- 保持应用场景,用于排错
- 遇到服务的问题时,可以把出问题的服务留下一台进行故障定位和场景分析->此时需要把这台机器从服务列表中拿下来,以免有新的请求进来造成服务的失败,这也是需要软负载中心直接使服务下线的一个场景.
- 优雅的停止应用
- 维护管理路由规则
- 对不同特性的数据进行拆分
- 软负载数据分组
- 数据分发与消息订阅的区别
- 从单机到集群
- 数据管理问题/连接管理问题
- 数据统一管理
- 数据聚合放在一个地方->软负载中心集群,无状态->对于数据发布者和订阅者来说,选择软负载中心集群中的任何一个机器连接皆可
- 把软负载中心集群中的机器的职责分开,即把聚合数据的任务和推送数据的任务分到专门的机器上处理->将软负载中心集群中有一台机器为软负载中心数据聚合,另一台机器为软负载中心数据推送->发布者和订阅者的连接是分开管理的->为了提升性能,在软负载中心负责数据推送的机器上是可以对聚合数据做缓存
- 数据对等管理方案
- 将数据分散在各个软负载中心的节点上并且把自己节点管理的数据分发到其他节点上,从而保证每个节点都有整个集群的全部数据并且这些节点的角色是对等的->使用软负载中心的数据发布者和数据订阅者只需要去连接软负载中心集群中的任何一台机器就可以->软负载中心集群内部,各个节点之间会进行数据的同步
- 批量处理同步->合并变化,同步一次
- 如果节点较多,同步量会较大->对集群内的节点进行指责划分
- 如果集群管理的总体数据很多,超过了单机限制->则需要对数据进行分组处理->让每个节点管理一部分数据->即用UI规则对数据进行类似分库分表的操作->则数据订阅者可能就需要连接多个数据分发节点了
- 将数据分散在各个软负载中心的节点上并且把自己节点管理的数据分发到其他节点上,从而保证每个节点都有整个集群的全部数据并且这些节点的角色是对等的->使用软负载中心的数据发布者和数据订阅者只需要去连接软负载中心集群中的任何一台机器就可以->软负载中心集群内部,各个节点之间会进行数据的同步
- 集中配置管理中心
- 集中配置管理中心结构
- 准备的持久存储来保存持久数据(Master-Slave)->一般采用关系型数据库->通过两个节点的主备来解决持久数据安全的问题.
- 集中配置管理中心集群这层由多个集中配置管理中心节点组成->对等->都可以提供数据给应用端等->互不依赖
- 集中配置管理中心的单个节点->部署了一个nginx和一个web应用->其中web应用主要负责完成相关的程序逻辑如数据库的相关操作以及根据ip等的分组操作,即整个应用的逻辑放在了web应用中;单机的本地文件Local File则是为了容灾和提升性能,客户端进行数据获取的时候,最后都是从nginx直接获取本地文件并把数据返回给请求端
- 集中配置管理中心的使用分为了以下两部分
- 提供给应用使用的客户端->主要是业务应用通过客户端去获取配置信息和数据,用于数据的读取
- 为控制台或者控制脚本提供管理SDK
- 包括了对数据的读写,通过管理SDK可以进行配置数据的更改
- 客户端实现和容灾策略
- 客户端通过http协议与集中配置管理中心进行通信
- 通过轮询获取最新数据_普通轮询
- 改进使用长轮询,Long Polling->如果没有数据,长轮询会等待;如果等待数据,立刻返回;如果一直没有数据则等到超时后返回,继续建立连接,而普通轮询就直接返回了->是HTTP普通轮询和Socket长连接方式的折中-
- 容灾
- 数据缓存
- 数据快照
- 本地配置
- 文件格式->如果是二进制数据格式,那么就没有对应的工具是无法对配置进行修改->如果客户端容灾退化到一个单机应用就会需要直接修改配置内容和数据->那么文本格式的限制就非常重要和关键了
- 客户端通过http协议与集中配置管理中心进行通信
- 服务端实现和容灾策略
- Nginx+Web应用->和逻辑相关的部分在Web应用上实现,Nginx用于请求的处理和最后结果的返回,而供返回的数据的都在本地文件系统中
- 和数据库的数据同步
- 数据库策略
- 数据库在设计时需要支持配置的版本管理,即随着配置内容的更改,老的版本是需要保留的,为了方便进行配置变更的对比和回滚->而数据库本身需要主备进行数据的容灾考虑
- 集中配置管理中心结构
构建大型网站的其他要素
- 加速静态内容访问速度的CDN
- CDN源站/CDN节点
- 网络缓存技术
- 几个关键技术
- 全局调度->需要根据用户地域、接入运营商以及CDN机房的负载情况去调度
- 缓存技术->提升命中率
- 内容分发
- 带宽优化
- 大型网站的存储支持
- 基本上就是在解决存储和计算的问题
- 关系型数据库
- 分布式文件系统
- 图片、大文本存储->使用数据库不合适
- NAS网络存储设备(Network Attached Storage),其本身的IO吞吐性能以及扩展性在大型网站中会出现比较明显不足
- 分布式文件系统具体产品
- 开源的淘宝的TFS
- 不开源的Google#GFS,Goole File System->GFS Client(负责从Master获取要操作的文件在ChunkServer中的具体地址,然后直接和ChunkServer通信,获取数据或者进行数据的写入、更新)/GFS Master(维护所有的文件系统元数据、控制整个系统范围内的一些活动、与ChunkServer之间通过周期性的心跳进行通信,检测对方是否在线)/GFS chunkserver(Data Node,文件存储的地方)
- 主要解决了单机文件存储容量及安全性的问题,把多台廉价pC组成一个大的分布式的看起来像文件系统的集群
- [HDFS,采用Java的类GFS的实现]
- NoSQL
- No SQL/Not Only SQL
- 基本上处于分布式文件系统和SQL关系型数据库之间的系统都被归为NOSQL的范畴
- 数据模型
- Key-Value,没办法进行高效的范围查询
- Ordered Key-Value,Key是有序的->可解决基于Key的范围查询的效率问题,不过在这个模型中,Value本身的内容和结构是由应用来负责解析和存储的->如果在多个应用中去使用则并不直观也不方便
- BigTable
- Google的结构化数据的分布式存储系统->Value是由多个Column Family组成
- Document,Full-Text Search
- 可以在Value中任意自定义复杂的Scheme/对索引方面的支持
- Graph
- 支持图结构的数据类型
- 系统结构
- [HBase]->借鉴Google BigTable的一个Java版本的开源实现
- 存储到HBase的数据是通过HRegionServer来管理的,每个HRegionServer管理了多个HRegion,每个Region管理具体的数据->HMaster是管理所有HRegionServer的节点,是一个中心控制的结构
- Amazon#Dynamo结构
- 采用了一致性哈希进行管理
- [Cassandra]是一个开源的类似Dynamo的实现
- [HBase]->借鉴Google BigTable的一个Java版本的开源实现
- 缓存系统
- 非持久的存储,是为了加速应用对数据的读取
- Redis和Memcache是两个使用很广泛的开源缓存系统->Redis已经有了对于集群的支持,也可以做单机的应用来使用;而memcache本身还是一个单机的应用,在使用时->集群->常见的是采用一致性哈希的方式
- 使用缓存的场景
- 使用缓存来降低对底层存储的读压力,需要注意缓存和数据存储中数据一致性问题
- 应用 <---> 缓存 <---> 存储
- 这种方式,应用是不直接操作存储,存储由缓存控制;对于缓存来说,需要保证数据写入缓存后能够存入存储中,所以缓存本身的逻辑会复杂些,需要有很多操作日志及故障恢复等
- 另一种方式,应用直接与缓存和存储进行交互。一般的做法是应用在写数据时更新存储,然后失效缓存数据;而在读数据时首先读缓存,如果缓存中没有数据,那么再去读存储,并且把数据写入缓存
- 第三种方式,对于全数据缓存比较合适,即当存储的数据变化时,直接从存储去同步数据到缓存中,以更新缓存数据
- 另一个重要场景是对于Web应用的页面渲染内容的缓存
- 具体的实现技术为ESI(Edge Side Includes)->通过在返回的页面中加上特殊的标签,然后根据标签的内容去用缓存进行填充的一个过程.
- 处理ESI标签的具体工作可以放在Java的应用容器中做,也可以放在Java应用容器前置的服务器做
- 搜索系统
- 站内搜索->网站的数据量和访问量很小时,一些数据的查询可以直接用数据库的Like操作来实现->实现效率低->不智能
- 爬虫问题->根据数据变化来更新索引
- 倒排索引
- 查询预处理
- 相关度计算
- 数据计算支撑
- 离线计算、实时计算
- 离线计算
- 把业务数据从在线存储中移动到离线存储中,然后进行数据处理的过程
- Google MapReduce模型
- Map阶段:根据设定的规则把整体数据集映射给不同的Worker来处理并且生成各自的处理结果
- Reduce阶段:对前面处理过的数据进行聚合,形成最后的结果
- [Hadoop]是MapReduce的一个开源实现.Hadoop使用HDFS进行数据存储,而[Spark]则提供了基于内存的集群计算的支持
- 在线计算
- 流式计算->Storm
- 发布系统
- 分发应用->需要提供自动高效并且容易操作的机制来把经过测试的程序包分发到线上应用->一般采用Web的操作方式
- 发布控制台->多机房->发布服务器
- 启动校验
- 应用重启启动后,需要进行校验从而完成这台应用服务器上的应用发布->对应用的校验通常是应用自身提供一个检测脚本或者页面,发布系统执行这个脚本或者访问页面后来判断返回的结果
- 停止应用时,需要优雅的关闭->需要在关闭应用前把这个应用从负载均衡或者软负载中心上移除
- 灰度发布
- 会对新应用进行分批发布,逐步扩大新应用在整个集群中的比例直至最后全部完成->这里讲的灰度发布主要是针对新应用在用户体验方面完全感知不到的更新.
- 产品改版Beta
- 提供新旧应用的共存
- 分发应用->需要提供自动高效并且容易操作的机制来把经过测试的程序包分发到线上应用->一般采用Web的操作方式
- 应用监控系统
- 能够及时了解应用的运行状况并能够进行相应的控制
- 监视和控制量部分
- 数据监视维度
- 系统数据和应用自身的数据->系统数据指的就是当前应用运行的系统环境的信息,如CPU使用率、内存使用情况、交换分区使用情况、当前系统负载、IO情况等;而应用自身的数据,则是不同应用有不同的数据,一般会是调用次数、成功率、响应时间、异常数量等维度的数据
- 数据记录方式
- 系统自身的数据已经被记录到了本地磁盘,应用的数据一般也是存放在应用自身的目录中,便于采集->也有直接把应用日志通过网络发送到采集服务器的情况,可以减轻本地写日志的压力
- 对于应用数据的记录,会考虑用定时统计的方式记录一些量很大的信息.如对于一个提供服务的应用,在没有特别需求时,并不直接记录每次调用的信息,而是会记录一段时间如5s或者一个间隔时间内的总调用次数、总响应时间这样写信息,而对于异常信息则每次都会予以记录;采用统计的方式是为了减小记录的大小以及对本地磁盘的写入压力
- 数据采集的方式
- 采集方式有应用服务器主动对同给监控中心以及等待监控中心来拉取两种方式->前者控制权在应用服务器上,可能出现的问题是应用服务器推送的压力超过采集的中心服务器的能力,会造成重试等额外开销并且需要应用服务器上的推送程序控制重试逻辑和当前传送位置等信息->后者把复杂性都放在中心采集服务器上处理,使得应用服务器中支持数据采集的部分变的简单
- 展现与告警
- 提供图表的形式可以提供Web页面的展示->通过手机应用来接收报警->比短信方式好
- 控制
- 应用启动后在运行期对于应用的行为改变->对于应用的运维,最低的要求是出现问题时可以通过重启应用解决,但是我们还是需要更加精细化的控制应用-降低和一些切换。降级是我们遇到大量请求且不能扩容的情况时所进行的功能限制的行为->而切换更多的是当依赖的下层系统出现故障并且需要手工进行切换时的一个管理,这些控制一般都是通过开关,参数设置来完成
- 数据监视维度
- 依赖管理系统
- 随着网站功能增多,应用的个数迅速增加,应用之间的关系也会越来越复杂,理清这些依赖关系并能够管理这些依赖会非常重要
- 一个应用在完成某个功能时到底需要依赖哪些外部系统、这些依赖中哪些是必要依赖,强依赖(登陆验证用户名和密码),哪些是有了更好没有也可以的依赖,弱依赖(如记录登陆时间和ip等)
- 动态检测和静态检测->动态检测的主要检查方式是模拟被调用系统不可用和响应慢的两种情况
- Google#Dapper,A Large-Scale Distributed Systems Tracing Infrastructure->traceId,index->形成一个调用时序图
- 多机房问题分析
- 同城机房和异地机房
- 同城多个机房中,对于重要的应用系统,会在不止一个机房中部署;而对于数据库系统,则会把主备放在不同机房->尽量避免不必要的跨机房的内部系统调用
- 为了数据安全,把产生的业务数据都同步到异地的机房->把一些对数据延迟不敏感的系统部署到异地,如只读系统.
- 同城机房和异地机房
- 系统容量规划
- 我们应该知道的信息就是整个系统的容量以及运行时所处的水位->我们把某个应用系统集群能够提供的并发能力和当前的压力比作一个水桶的容量和水位->那么准确知道各个系统的容量和当前高峰时的水位是一件很重要的事情->因为我们还是希望优先通过扩大容量来支持更多的请求而不是首选降级的方案.
- 考虑过去的增长情况并结合人为的判断
- 弄清楚当前系统高峰期的水位
- 弄清楚当前各个系统的容量
- 通过测试->压力测试
- 设置警戒值,高峰水位搞过警戒值就增加容量,保持高峰的水位是低于警戒值的
- 内部私有云
总结:
可以将一些设计思路、方法等应用到游戏服务器整体架构设计当中(主要是登陆、支付等http服务).