大型网站系统与java中间件

第一章 分布式系统介绍

为什么要有分布式系统?

1.升级单机处理能力的性价比越来越低

2.单机处理能力存在瓶颈

3.处于稳定性和可用性的考虑

线程与进程的执行模式

1.互不通信的多线程模式

2.基于共享容器协同的多线程模式

3.通过事件协同的多线程模式

4.多进程模式

网络通讯

BIO,NIO,AIO

从单机到分布式

引入负载均衡,DNS,存储的变化(kv存储等)

分布式系统的难点

1.缺乏全局时钟

2.面对故障独立性

3.处理单点故障

4.事务的挑战

第二章 大型网站及其架构演进过程

1.单机系统
2.单机负载告警,数据库与应用分离
3.应用服务器告警,让应用服务器走向集群
1)引入负载均衡
2)接机session问题
    session sticky
    session replication
    session 数据集中存储
    cookie based(将session的数据存到客户端的浏览器中,会有安全隐患)

4.数据库压力变大,读写分离

    读写分离带来的问题,数据复制的问题(延迟),应用对数据源的选择问题

    搜索引擎其实是一个读库

    数据缓存,页面缓存

5.引入分布式存储系统

6.读写分离后又遇到瓶颈,分库分表

    读写分离解决的是读压力大的问题,对于数据量大或者更新量的情况不起作用

    垂直拆分  把数据库中不同的业务数据拆分到不同的数据库中

    水平拆分  把同一个表的数据拆到两个数据库中

    分库之后需要解决事务的问题,或者去掉事务的关联,或者使用分布式事务

7.数据库问题解决后,应用面对的挑战

    拆分应用

    走向服务化的路

    消息中间件

第三章 构建java中间件

垃圾回收与内存堆布局

java并发编程的类,接口和方法

    1.线程池

    2.synchronized

    3.volatile

    4.atomics

    5.wait,notify,notifyAll

    6.CountDownLatch

    7.CyclicBarrier

    8.Semaphore

    9.Exchanger

    10.Future和FutureTask

    11.并发容器(ConcurrentHashMap,CopyOnWriteList等)

动态代理

反射

网络通讯实现选择(BIO,NIO,AIO)

第四章 服务框架

将应用拆分,或者增加一个中间的服务层

服务化的方式好处是使得系统整体架构更立体了,而且一些散落在多个应用系统中的代码也成了服务可以由专门的团队进行维护

//从单机变成分布式后的代码变化
//客户端
public int add(int a, int b) {
    //获取可用服务地址列表
    List<String> list = getAvailableServiceAddress("Caluctor.add")
    
    //确定要调用服务的目标机器
    String address = chooseTarget(list);
   
    //建立连接
    Socket s = new Socket(address);
    
    //请求的序列化
    byte[] request = genRequest(a,b);

    //发送请求
    s.getOutputStream().write(request);

    //接收结果
    byte[] response = new byte[10240];
    s.getInputStream().read(response);

    //解析结果
    int result = getResult(response);
   
    return result;
}

服务端实现

public class EventHandler {
    
    public static class Request {
        public Socket socket;
        public String serviceName;
        public String serviceVersion;
        public String methodName;
        public Object[] args;
    }

    public static void eventHandler() {
        while(true) {
            byte[] requestData = receiveRequest();
            Request request = getRequest(requestData);
            Object service = getServiceByNameAndVersion(
                                       request.serviceName,request.serviceVersion);
             Object result = callService(service,request.methodName,
                                       request.args);
             byte[] data = genRequet(result);
             request.socket.getOutputStream().write(data);
        }
    }

}

服务调用端具体工作流程

1.调用发起

2.寻址路由

3.协议适配/序列化

4.网络传输

5.反序列化/协议解析

6.得到结果返回给调用方

服务框架的使用

1.可以用使用spring来引入服务框架

2.服务框架与应用和容器的关系

    1)服务框架作为web应用的扩展也就是jar包,这样使用方面但是升级不方便

    2)服务框架是web容器的一部分(也相当于一个werb应用)

    3)服务框架作为web容器本身,这就需要有优先级更高的classloader优先加载服务框架

服务调用者与服务提供者之间通讯方式的选择

1.直接通讯

2.采用透明代理方式

3.引入基于接口,方式,参数的路由(要有负载均衡的作用将请求均摊到后端服务上)

4.多机场场景,当某个机房不可用时可以自动切到正常的机房

服务调用端的流量控制

1.根据服务端自身的接口,方法做控制,也就是不同的借口,不同的方法设置不同的阈值

2.根据来源控制

序列化和反序列化的处理

1.java本身是支持序列化/反序列化的,但如果要使用java之外的语言则需要考虑跨语言的支持

2.序列化/反序列化的性能开销,以及不同方式的性能比较也是需要注意的问题

3.需要注意序列化后的长度。也就是需要在易用性,跨语言,性能,序列化后数据长度等方面综合考虑

我们可以使用HTTP协议,然后消息使用XML或者json

网络通讯实现选择

BIO,NIO,AIO

支持多种异步调用方式

1.Oneway

2.Callback

3.Future

4.可靠异步

使用Future优化对远程服务的调用,将多个请求合并一起发送(这些请求都是NIO异步的所以可以合并),但是需要注意这些请求之间不能有依赖,如果A请求依赖B请求的结果,则只能串行。然后将异步收集到的结果合并即可,这样等待的总时间就等于处理时间最长的那个访问,可大幅度提供性能。

服务升级

1.接口不变,最简单直接内部升级即可

2.在接口中增加新方法,也比较简单直接增加新方法调用即可

3.接口参数改变,最好的办法是增加一个版本号。老的调用还是走老版本,新调用访问新版本

实战中的优化

1.服务的拆分,提取公有的服务

2.服务的粒度,需要根据业务的实际情来划分服务

3.优雅和实用的平衡,如果某个逻辑需要大量调用,可能需要在调用者前面暴露一个缓存,从架构上说不够

   优雅,但是却很实用

4.分布式环境中的请求合并,因为实用了NIO,多个请求之间如果没有依赖可以异步一次发起多个请求然后

   把他们的结果合并在一起

服务框架和ESB(企业服务总线)的区别

1.服务框架是一个点对点的模型,而ESB是一个总线式的模型

2.服务框架基本上是面向同构系统,不会重点考虑整合的需求,而ESB会更多的考虑不同厂商所提供服务的

    整合

第五章 数据访问层

在不升级硬件的情况下,能够想到的处理方案有

1.优化应用,减少数据库的压力

2.增加缓存,搜索引擎,减少对数据库的压力

3.把数据库的数据和访问分到多台数据库上

垂直拆分,把一个数据库中不同业务的数据分到不同的数据库里面,影响如下

1.单机的ACID被打破了,原来单机的事务逻辑会受到影响,那么放弃原来的单机事务修逻辑或者用分布式事务

2.一些join会比较困难,需要用应用和其他方式来解决

3.靠外键去进行约束的场景会受到影响

水平拆分,根据一定的规则把同一业务单元的数据分到多个数据库中,影响如下

1.同样有ACID被打破的情况

2.同样有join被影响的情况

3.靠外键去进行约束的场景有影响

4.依赖单库的自增序列生成唯一ID会受影响

5.针对单个逻辑意义上的表查询要跨库了

分布式事务

两阶段提交

CAP理论

BASE理论

paxos协议

WRN协议

向量时钟

水平分库的多级主键sequeue问题

唯一性和连续性

如果使用UUID等方案可以保证唯一性,但是连续性就不好了

id放在统一的地方管理,带来的问题是

1.每次插入数据还多多查询一次ID有性能问题

2.id生成器作为一个无状态的集群存在,其可靠性要靠整个集群来保证

3.存储问题,需要根据不同类型进行对应的容灾方案

另一种改变是直接把ID生成器舍掉,把相关的逻辑放到应用本身就行了

跨库join问题

1.把原来join的操作分成多次的数据库操作,也就是先查一个表,再查另外一个表

2.数据冗余

3.借助外部系统(搜索引擎)解决一些跨库问题

外键约束

比较难解决,不能完全依赖数据库本身

跨库查询带来的问题

1.排序,多个来源的数据查询出来以后,在应用层进行排序工作

2.函数处理,继max,sum等对多数据源的值进行相应的处理

3.求平均值,从多个数据源来源进行查询时,需要把sql改成查询sum和count然后对多个来源数据求和再计算

4.排序分页,是等步长(在每个分页中来自不同数据源的记录数是一样的)的在多个数据源上分页处理,

    还是等比列(来自不同数据源的数据占这个书院符合条件的数据总比列一样)的分页处理

5.排序后分页,就是把排序和分页放在一起的情况是最复杂的

提供数据访问层方式

1.为用户提供专有API,这种不推荐通用性很差

2.使用通用方式JDBC,迁移成本很低

3.基于ORM或者ORM接口的方式,这种方式介于上面两者之间。 

从数据层的整理流程看有以下几个步骤

1.SQL解析

    是否需要执行所有SQL,是否需要执行方言

2.规则处理

    固定hash,一致性hash,需要节点一致性hash,映射表与规则自定义计算方式

3.SQL改写

    分库之后查询就需要修改表名了,此外计算平均值(不能从多源去平均值再计算平均值的平均值)

4.数据源选择

    决定了分组之后要确定访问哪个分库

5.SQL执行

6.结果集返回合并处理

    实际生产环境会有多个数据源,配置文件中就需要配置多个源会很麻烦,这部分需要做合并或者抽象出来

独立部署时候如果采用代理访问,客户端和proxy之间的协议有两种

1.数据库协议,缺点是没法做到连接复用,而且重新实现一套proxy代价很高,不过用JDBC就很容易部署

2.私有协议,需要自己实现,但是可以做到连接复用 

读写分离

1.数据结构相同,多从库对应一主库的场景

2.主/备库分库方式不同的数据复制,比如主库可能需要根据ID取模然后复制到不同的从库中

3.引入数据变更平台

数据平滑迁移

1.确定要开始扩容,并且开始记录数据库的数据变更的增量日志

2.接下来,数据开始复制到新表,并且也有更新进来。增量日志和新库中可能不一致

3.当全量迁移结束后,我们把增量日志中的数据也进行迁移,此时仍然不一致但是会慢慢收敛

4.我们进行数据对比,这是可能会有新库数据和源库数据不同的情况,把他们记录下来

5.接着我们停止源数据库中对于要迁移走的数据的写操作,然后进行增量日志的处理

6.最后更新路由规则,所有新数据的读或写就到了新库表,这样就完成了整个迁移过程

第六章 消息中间件

消息中间件为我们带来了异步的特性,对系统进行了解耦

比如一个登陆系统,登陆成功发送短信通知,安全系统记录等等,这些模块都是附带的不应该影响正常登陆

可以通过消息中间件来解决这些问题

java的消息中间件JMS是javaEE中的一个关于消息的规范

JMS的定义

JMS Common PTP Domain Pub/Sub Domain
ConnectionFactory QueueConnectionFactory TopicConnectionFactory
Connection QueueConnection TopicConnection
Destination Queue Topic
Session QueueSession TopicSession
MessageProducer QueueSender TopicSender
MessageConsumer QueueReceiver TopicReceiver

不使用JMS完成发送消息一致性的方案

1.业务处理应用胡搜西安把消息发给消息中间件,标记消息的状态为待处理

2.消息中间件收到消息后,把消息保存在消息存储中,并不投递该消息

3.消息中间件返回消息处理结果(仅是入库的结果),结果是成功或失败

4.业务方收到消息中间件返回的结果并进行处理

    a)如果收到的是失败,那么就放弃业务处理结束

    b)如果收到的是成功,则进行业务自身的操作

5.业务操作完成,把业务操作的结果发送给消息中间件

6.消息中间件收到业务操作结果,根据结果进行处理

    a)如果业务失败,则删除消息存储中的消息结束

    b)如果业务成功,则更新消息存储中的消息状态为可发送,并且进行调度进行消息的投递

可能需要业务方主动询问消息是否送达了,也就是消息补充机制,这样才能保持消息存储的可靠

一个固化的流出,伪代码如下

Result postMessage(Message, PostMessageCallback) {
    //发送消息给消息中间件
    //获取返回结果
    //如果失败,返回失败
    //进行业务操作
    //获取业务操作结果
    //发送业务操作结果给消息中间件
    //返回处理结果
}

消息中间件和使用者之间的强依赖关系

如果消息中间件失败会导致正常的业务无法继续

1.提供消息中间爱你见系统的可靠性,但是没办法保证百分百可靠

2.对于消息中间件系统中影响业务操作进行的部分,使其可靠性与业务自身的可靠性相同

3.可以提供弱依赖的支持,能够较好的保证一致性

把消息中间件的存储入内放到业务相同的库中有如下影响

1.需要用业务自己的数据库承载消息数据

2.需要让消息中间件去访问业务数据库

3.需要业务操作的对象是一个数据库,或者说支持事务的存储,并且这个存储必须能够支持消息中间件需求

消息存储的可靠性

    持久存储部分的代码完全自主实现,   利用现有的存储系统实现

1.实现基于文件的消息存储

2.采用数据库作为消息存储

3.基于双机内存的消息存储(两台机器都是先写入内存一旦有一个机器宕机就立刻写入磁盘,如果两个机器同时

   宕机则不行)

消息中间件的扩容

1.消息中间件自身的扩容,通过更新路由的方式完成扩容

2.消息存储的扩容,如果不保存消息顺序就可以轻松扩容

产生重复消息原因

1.消息发送端重复发送,应用发送超时(负载变高,网络等),消息中间件存储返回错误导致重复发送,

2.消息中间件向外投递产生重复,应用收到消息处理后没有响应(出问题,网络,超时等)

    消息中间件存储错误认为还未处理

解决这种问题的最好办法就是 消息处理是冥等的关系

JMS的消息确认方式与消息重复关系

1.AUTO_ACKNOWLEDGE     自动确认的方式

2.CLIENT_ACKNOWLEDGE  客户端自己确认的方式

3.DUPS_OK_ACKNOWLEDGE   接收消息方处理函数执行结束后进行确认

消息接收者对于消息的接收会出现下面两种情况

1.at least once(至少一次)

2.at most once(至多一次)

消息投递的其他属性

1.消息优先级

2.订阅者消息处理顺序和分级订阅

3.自定义属性

4.局部顺序(比如转账交易)

单机多队列优化

设置逻辑上连续的队列,然后索引到磁盘,磁盘上存储的数据不是连续的

这些写是连续的, 但是读却是随机的(尽可能命中PAGECACHE减少IO操作)

读一个消息时,先读逻辑队列再读物理队列增加开销

需要保证物理队列与逻辑队列完全一致增加复杂性

解决本地消息存储的可靠性

1.把单个消息中间件变成master和slave两个节点,slave节点订阅master上消息,这样会有延迟

2.master同步复制等到slave节点返回的成功后才向应用返回成功

对于消息数据安全性要求非常严格的场景可以采用第二种

消息队列的扩容

1.在元队列开始扩容后需要有一个标志,即便有新消息过来也不再接收

2.通知消息发送端新的队列位置

3.对于消息接收端,对原来队列的定位会收到新旧两个位置,当旧队列的数据接收完毕后则会只关心新队列位置

Push和Pull方式的对比

  Push Pull
数据传输状态 保存在服务端 保存在消费端
传输失败,重试

服务端需要维护每次传输状态,

遇到失败情况需要重试

不需要
数据传输实时性

非常实时

默认的短轮询方式的实时性依赖与

Pull间隔时间,间隔越大实时性越低

长轮询模式的实时性与Push一致

流控机制

服务端需要依据订阅者的消费能力

做流控

消费端可以根据自身消费能力决定

是否去Pull消息

第七章 软负载中心与集群配置管理

软负载中心有两个基础职责

1.聚合地址信息,无论是服务框架中需要用到的服务提供者地址,还是消息中间件系统中的消息中间件应用

    的地址,都需要由软付贼中心去聚合地址列表

2.生命周期感知,软负载中心需要能对服务的上下线自动感知,并且根据这个变化去更新服务地址数据,

    形成新的地址列表后,把数据传给需要数据的调用者或者消息的发送者和接收者。

服务上下线的感知

1.软负载中心通过客户端的心跳包感知(通过客户端和服务端维持一个长连接),但如果服务端负责高或者网络

    等原因会导致错误的认为客户端下线了

2.当一段时间负载中心没有收到心跳包,就将这个检测再交给另外一个系统去核实(就是补偿检查方案)但是也会

    出现这个独立的检查系统网络有问题

软负载中心分发和消息中间件订阅的区别

1.消息中间件需要保证每条消息必须收到,软负载中心只要保证最新的消息收到即可

2.对于消息中间件一个集群所有机器共享消息所以只要一个机器收到即可,软负载中心维护的是所有机器的配置

   所以需要将配置分发到集群的所有机器上

提升数据分发性能

1.数据压缩

2.全量与增量的选择

提供自动感知以外的上下线开关

1.优雅的停止应用,通过软负载中心停止流量到这个机器上

2.保持应用场景,用于排错

从单机到集群

数据管理问题,软负载中心维护的机器列表数据如何维护,存储

连接管理问题,所有的数据发布者和数据订阅者都会连接到这台软负载中心上,从单机到集群怎么管理?

数据管理方案

1.软负载中心集群共享一个聚合数据,发布者和订阅者连接软负载中心

2.软负载中心集群之间每台机器网状形式拷贝数据,缺点是机器多了数据流量会很大

3.同上,集群之间机器分组拷贝数据,订阅者软负载集群之间拷贝,发布者软负载之间拷贝,节省流量

按数据是否持久分两个维度考虑

1.持久的是指数据本身与发布者生命周期无法的,如路由规则,数据访问层的分库分表规则和数据库配置等

2.非持久订阅是和发布者生命周期有关的,如服务器的地址列表等

对于集群配置管理中心来说,最关心的是稳定性和各种异常情况下的容灾策略,其次是性能和数据分发延迟

客户端容灾

1.数据缓存,本地保存一份拷贝,不一定是最新的但是能保证可用

2.数据快照,保存的是历史数据的版本

3.本地配置,一旦软负载中心不可用,优先使用本地配置

服务端容灾

需要和数据库的数据同步,定期检查服务端的数据和数据库是否一致

第八章 构建大型网站的其他要素

CDN 是内容分发网络的缩写

通过浏览器访问一个网站的大致过程

1.用户向浏览器提交要访问的域名

2.浏览器对域名进行解析,由于CDN对域名解析过程进行了调整,所以得到的是该域名对应CNAME记录

3.对CNAME再次进行解析,得到实际IP地址。在这次的解析中,会使用全局负载均衡DNS解析,也就是我们需要返还具体IP地址,需要根据地址位置信息以及所在ISP来确定返回的结果,这个过程才能让身处于不同地域,连接不同接入商的用户得到最合适最忌访问的CDN地址,才能做到就近访问,从而提升速度

4.得到实际的IP地址以后,向服务器发出访问请求

5.CDN会根据请求的内容是否在本地缓存进行不同处理

   如果存在则直接返回结果

   如果不存在,则CDN请求源站获取内容再返回结果

CDN中的几个关键技术

1.全局调度,有一个IP地址识别库将最近的用户分配到合适的CDN机器,如果某CDN机房负载很高或者机房

    出现事故,需要将请求能定位到其他机房中

2.缓存技术,缓存命中率

3.内容分发

4.带宽优化,所有的请求流量都压到了CDN上,如何节省带宽

存储支持

分布式文件系统

GFS

NoSQL

key-vlaue     无序的键值对

Ordered key -value

key-vlaue 无序的键值对
Ordered key -value 有序的键值对
BigTable 列数据库
Document,Full Text Search 文档数据库
Graph

图数据库,图数据库

再往后发展就是关系型

数据库

缓存系统

搜索系统

爬虫

    定时从数据源中拉数据

    通过数据变更的通知及时构建索引

    倒排索引

数据计算支持

    离线计算和实时计算   

发布系统

    分发应用

    启动校验,需要在负载均衡上做文章,在关闭应用前把流量导入其他服务器,应用启动后在将流量导回来

    灰度发布

    产品改版beta,提供新旧应用的共存

应用监控系统

依赖管理系统(谷歌的Dapper可以跟踪请求在分布式系统中调用过程)

多机房问题

系统容量规划(多少机器能撑得住多少流量)

内部私有云

猜你喜欢

转载自xxniao.iteye.com/blog/2238996