0%

  • 模块结构
    sharding-core-preprocess的主要功能是对SQLStatment对象进行预处理生成SQLStatementContext对象,为路由提供必要信息。按功能分类可以分成三个部分:
  1. org.apache.shardingsphere.core.optimize。SQLStatementContextFactory类,主要作用根据传入的SQLStatement类型创建相应的SQLStatementContext对象。创建SelectSQLStatementContext、InsertSQLStatementContext、CommonSQLStatementContext三种。
  2. org.apache.shardingsphere.core.optimize.statement。包含SQLStatementContext接口和SelectSQLStatementContext、InsertSQLStatementContext、CommonSQLStatementContext三种实现类。存储:TablesContext、SQLStatement和其他类型Context。
  3. org.apache.shardingsphere.core.optimize.segment。内部分insert、select、table三种类型,包含segment对应的context。
  • 模块结构图
  • 主要逻辑流程和关键类分析
    sharding-core-preprocess的主要逻辑是从SQLStatementContextFactory.newInstance()方法开始的,主逻辑流程图如下。其中,虚线箭头表示从属关系,实线表示执行顺序。
  • 设计模式和设计原则
  1. 简单工厂模式
    SQLStatementContextFactory.newInstance()方法根据sqlStatement的类型来决定要创建SQLStatmentContext实例。SQLStatementContext是一个接口,它拥有SelectSQLStatementContext、InsertSQLStatementContext、CommonSQLStatementContext这三个实现类。

  • 模块结构
    sharding-core-route的主要功能是将解析生成的SQLStatement对象,进行预处理和路由生成SQLRouteResult对象。这里只讲解路由部分,预处理部分单独一片文章讲解。按功能分类可以分成四个部分:
  1. org.apache.shardingsphere.core.route.hook。hook相关的类,用于跟踪和分析。
  2. org.apache.shardingsphere.core.route.router。包含分片路由和主备路由两个逻辑的实现,分别位于ShardingRouter和ShardingMasterSlaveRouter,生成SQLRouteResult对象
  3. org.apache.shardingsphere.core.route.type。主要包含RoutingEngine的接口和具体类的实现,实现了route()方法,完成了在具体SQLstatementContext、ShardingRule、ShardingCondition中完成分片操作,生成RoutingResult。支持ShardingRouter和ShardingMasterSlaveRouter的方法实现。
  4. org.apache.shardingsphere.core.route。主要的类为:StatementRoutingEngine和PreparedStatementRoutingEngine。这两个类是核心,路由逻辑的入口在它的route()方法中。前者提供 public SQLRouteResult route(final String logicSQL),后者提供public SQLRouteResult route(final List parameters)。
  • 模块结构图
  • 主要逻辑流程和关键类分析
    sharding-core-route的主要逻辑是从StatementRoutingEngine.route(final String logicSQL)开始的,主逻辑流程图如下。其中,虚线箭头表示从属关系,实线表示执行顺序。

    上图中的logicSQL的位置和语句对应有偏差,下方图使用EXCEL更准确描述。
  • 设计模式和设计原则
  1. 简单工厂模式+依赖倒转原则
    RoutingEngine抽象出来route()方法,具体的实现类来实现具体的方法,每个实现类实现自己的路由算法。产生的实现类依据RoutingEngineFactory.newInstance(final ShardingRule shardingRule, final ShardingSphereMetaData metaData, final SQLStatementContext sqlStatementContext, final ShardingConditions shardingConditions)方法中的传入参数。具体代码位于ShardingRouter.route(final String logicSQL, final List parameters, final SQLStatement sqlStatement)方法中。

    1
    2
    RoutingEngine routingEngine = RoutingEngineFactory.newInstance(shardingRule, metaData, sqlStatementContext, shardingConditions);
    RoutingResult routingResult = routingEngine.route();
  2. 简单工厂模式
    简单工厂模式在模块中若干处有出现,例如:org.apache.shardingsphere.core.route.router.sharding.validator包内和org.apache.shardingsphere.core.route.router.sharding.condition.generator包内。

  3. 依赖倒转
    依赖倒转强调面向接口编程而不是面向实现编程,这样使得模块之间耦合度降低,方便以后扩展。org.apache.shardingsphere.core.route.type包内的RoutingEngine接口和若干实现类。方便以后新增路由引擎和修改路由引擎。

  • 模块结构
    sharding-core-parse的主要功能是将sql语句执行分片或加密操作生成最后的SQLStatement对象,供后面的模块调用。内部有7个子模块,按功能分类可以分成四个部分:
  1. sharding-core-parse-common,实现了SQL解析的主要功能。
  2. sharding-core-parse-mysql,sharding-core-parse-oracle,sharding-core-parse-postgresql,sharding-core-parse-sqlserver。这四个模块分别对应四个不同的数据库类型,里面包含该类型数据库特有的一些实现类。
  3. sharding-core-parse-spi,主要定义了两个接口。SQLParser,抽象了antlr4生成的语法解析器的解析方法execute()。SQLParserEntry,抽象了获取antlr4生成的词法解析器和语法解析器的方法。
  4. sharding-core-parse-test,主要包含了对sharding-core-parse模块内部主要功能的测试,使用jaxb技术来定义测试用例和测试结果。
  • 模块结构图
  • 主要逻辑流程和关键类分析
    sharding-core-parse的主要逻辑是从SQLParseEngine.parse()开始的,主逻辑流程图如下。其中,虚线箭头表示从属关系,实线表示执行顺序。
  • 设计模式和设计原则
  1. 享元模式
    SQLParseEngineFactory的功能是创建SQLParseEngine。它使用了享元模式,避免大量实例化相同类的开销,通过共享大幅度地减少单个实例的数目。SQLParseEngineFactory的getSQLParseEngine()方法显示了享元模式的主要逻辑,首先判断ENGINES(Map类型)里面是否含有databaseType.getName()相应的元素,如果有则直接返回,否则根据databaseType来创建SQLParseEngine对象,缓存在ENGINES中,并返回此对象。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public static SQLParseEngine getSQLParseEngine(final DatabaseType databaseType) {
    if (ENGINES.containsKey(databaseType.getName())) {
    return ENGINES.get(databaseType.getName());
    }
    synchronized (ENGINES) {
    if (ENGINES.containsKey(databaseType.getName())) {
    return ENGINES.get(databaseType.getName());
    }
    SQLParseEngine result = new SQLParseEngine(databaseType);
    ENGINES.put(databaseType.getName(), result);
    return result;
    }
    }
  2. 依赖倒转原则
    依赖倒转强调面向接口编程而不是面向实现编程,这样使得模块之间耦合度降低,方便以后扩展。org.apache.shardingsphere.core.parse.core.fillerorg.apache.shardingsphere.core.parse.core.extractororg.apache.shardingsphere.core.parse.sql.segmentorg.apache.shardingsphere.core.parse.sql.statement等。这些包的内部结构体现了这一原则。比如SQLStatementFillerEngine类的fill()方法内部调用SQLSegmentFiller接口,SQLSegmentFiller接口的实现类定义在impl包中。
    依赖倒转还体现在不同的数据库模块的实现。sharding-core-parse-common定义了SQLSegmentExtractor和SQLSegmentFiller接口,sharding-core-parse-spi定义了SQLParser接口和SQLParserEntry接口。,sharding-core-parse-mysql,sharding-core-parse-oracle,sharding-core-parse-postgresql,sharding-core-parse-sqlserver这四个模块会不同程度地实现上面的接口,不同的数据库都有各自特点,需要实现自身独有的实现类。依赖倒转为此提供了便捷,使得不同数据库只关注自身的实现。
  3. 工厂模式
    SQLParserFactory类的newInstance()体现了简单工厂的设计思想。它将对象的创建和使用分离开,对使用者屏蔽了复杂的创建过程。相关代码和注释如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public static SQLParser newInstance(final DatabaseType databaseType, final String sql) {
    //NewInstanceServiceLoader.newServiceInstances()返回一个装满SQLParserEntry类型对象的List
    //for循环遍历这个List
    for (SQLParserEntry each : NewInstanceServiceLoader.newServiceInstances(SQLParserEntry.class)) {
    //DatabaseTypes.getActualDatabaseType()根据名字返回对应databaseType,如果each的databaseType能够与传入的参数databaseType匹配则进行下面的计算
    if (DatabaseTypes.getActualDatabaseType(each.getDatabaseType()) == databaseType) {
    //创建SQLParser对象
    return createSQLParser(sql, each);
    }
    }
    throw new UnsupportedOperationException(String.format("Cannot support database type '%s'", databaseType));
    }

一、进程

  传统上,Unix操作系统下运行的应用程序、 服务器以及其他程序都被称为进程,而Linux也继承了来自unix进程的概念。必须要理解下,程序是指的存储在存储设备上(如磁盘)包含了可执行机器指 令(二进制代码)和数据的静态实体;而进程可以认为是已经被OS从磁盘加载到内存上的、动态的、可运行的指令与数据的集合,是在运行的动态实体。这里指的 指令和数据的集合可以理解为Linux上ELF文件格式中的.text .data数据段。

二、进程组

  每个进程除了有一个进程ID之外,还属于一个进程组,那什么是进程组呢?

  顾名思义,进程组就是一个或多个进程的集合。这些进程并不是孤立的,他们彼此之间或者存在父子、兄弟关系,或者在功能上有相近的联系。每个进程都有父进程,而所有的进程以init进程为根,形成一个树状结构。

  那为啥Linux里要有进程组呢?其实,提供进程组就是为了方便对进程进行管理。假设要完成一个任务,需要同时并发100个进程。当用户处于某种原因要终止 这个任务时,要是没有进程组,就需要手动的一个个去杀死这100个进程,并且必须要严格按照进程间父子兄弟关系顺序,否则会扰乱进程树。有了进程组,就可以将这100个进程设置为一个进程组,它们共有1个组号(pgrp),并且有选取一个进程作为组长(通常是“辈分”最高的那个,通常该进程的ID也就作为进程组的ID)。现在就可以通过杀死整个进程组,来关闭这100个进程,并且是严格有序的。组长进程可以创建一个进程组,创建该组中的进程,然后终止。只要在某个进程组中一个进程存在,则该进程组就存在,这与其组长进程是否终止无关。

   进程必定属于一个进程组,也只能属于一个进程组。 一个进程组中可以包含多个进程。 进程组的生命周期从被创建开始,到其内所有进程终止或离开该组。

  内核中,sys_getpgrp()系统调用用来获取当前进程所在进程组号;sys_setpgid(int pid, int pgid)调用用来设置置顶进程pid的进程组号为pgid。

三、作业

  Shell分前后台来控制的不是进程而是作业(Job)或者进程组(Process Group)。一个前台作业可以由多个进程组成,一个后台也可以由多个进程组成,Shell可以运行一个前台作业和任意多个后台作业,这称为作业控制。

  作业与进程组的区别:如果作业中的某个进程又创建了子进程,则子进程不属于作业。一旦作业运行结束,Shell就把自己提到前台,如果原来的前台进程还存在(如果这个子进程还没终止),它自动变为后台进程组。

四、会话

  再看下会话。由于Linux是多用户多任务的分时系统,所以必须要支持多个用户同时使用一个操作系统。当一个用户登录一次系统就形成一次会话 。一个会话可包含多个进程组,但只能有一个前台进程组。每个会话都有一个会话首领(leader),即创建会话的进程。 sys_setsid()调用能创建一个会话。必须注意的是,只有当前进程不是进程组的组长时,才能创建一个新的会话。调用setsid 之后,该进程成为新会话的leader。

  一个会话可以有一个控制终端。这通常是登陆到其上的终端设备(在终端登陆情况下)或伪终端设备(在网络登陆情况下)。建立与控制终端连接的会话首进程被称为控制进程。一个会话中的几个进程组可被分为一个前台进程组以及一个或多个后台进程组。所以一个会话中,应该包括控制进程(会话首进程),一个前台进程组和任意后台进程组。

  一次登录形成一个会话

  一个会话可包含多个进程组,但只能有一个前台进程组

五、控制终端

  会话的领头进程打开一个终端之后, 该终端就成为该会话的控制终端 (SVR4/Linux)

  与控制终端建立连接的会话领头进程称为控制进程 (session leader)

  一个会话只能有一个控制终端

  产生在控制终端上的输入和信号将发送给会话的前台进程组中的所有进程

  终端上的连接断开时 (比如网络断开或 Modem 断开), 挂起信号将发送到控制进程(session leader)

  进程属于一个进程组,进程组属于一个会话,会话可能有也可能没有控制终端。一般而言,当用户在某个终端上登录时,一个新的会话就开始了。进程组由组中的领头进程标识,领头进程的进程标识符就是进程组的组标识符。类似地,每个会话也对应有一个领头进程。

  同一会话中的进程通过该会话的领头进程和一个终端相连,该终端作为这个会话的控制终端。一个会话只能有一个控制终端,而一个控制终端只能控制一个会话。用户通过控制终端,可以向该控制终端所控制的会话中的进程发送键盘信号。

   同一会话中只能有一个前台进程组,属于前台进程组的进程可从控制终端获得输入,而其他进程均是后台进程,可能分属于不同的后台进程组。

  当我们打开多个终端窗口时,实际上就创建了多个终端会话。每个会话都会有自己的前台工作和后台工作。

分布式服务场景下,业务系统对于数据库中主键ID的需求主要有以下:
1.全局唯一性:不能出现重复的ID号。
2.趋势递增:在MySQL InnoDB引擎中使用的是聚集索引,由于多数RDBMS使用B-tree的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写入性能。
3.单调递增:保证下一个ID一定大于上一个ID,例如事务版本号、IM增量消息、排序等特殊需求。
4.信息安全:如果ID是连续的,恶意用户的扒取工作就非常容易做了,直接按照顺序下载指定URL即可;如果是订单号就更危险了,竞对可以直接知道我们一天的单量。所以在一些应用场景下,会需要ID无规则、不规则。
5.可靠性:业务还对ID号生成系统的可用性要求极高,如果ID生成系统瘫痪,整个业务系统都将瘫痪。

针对上述需求,ID生成系统的目标应为:
1.平均延迟和TP999延迟都要尽可能低;
2.可用性5个9;
3.高QPS。
leaf-segment方案和leaf-snowflake方案分别实现了:1235和1245.

  1. leaf-segment方案
  2. 1该方案需要数据库有一个专用的表来存储各个主键的相关信息。
    biz_tag,max_id,step,desc,update_time。重要字段说明:biz_tag用来区分业务,max_id表示该biz_tag目前所被分配的ID号段的最大值,step表示每次分配的号段长度。原来获取ID每次都需要写数据库,现在只需要把step设置得足够大,比如1000。
    例如:biz_tag=order,max_id=3000,step=1000.代表order表的主键目前已经使用的id到达3000,下一次再获取1000个id后,则max_id为4000。proxy-server每次从数据库取1000个id,然后用来相应业务服务的id请求。
  3. 2
    proxy-server采用双buffer。当号段消费到某个点时就异步的把下一个号段加载到内存中。而不需要等到号段用尽的时候才去更新号段,不需要在DB取号段的时候阻塞请求线程。这样做就可以很大程度上的降低系统的TP999指标。
    Leaf服务内部有两个号段缓存区segment。当前号段已下发10%时,如果下一个号段未更新,则另启一个更新线程去更新下一个号段。当前号段全部下发完后,如果下个号段准备好了则切换到下个号段为当前segment接着下发,循环往复。
    每个biz-tag都有消费速度监控,通常推荐segment长度设置为服务高峰期发号QPS的600倍(10分钟),这样即使DB宕机,Leaf仍能持续发号10-20分钟不受影响。
    每次请求来临时都会判断下个号段的状态,从而更新此号段,所以偶尔的网络抖动不会影响下个号段的更新。

1.3这种模式有以下优缺点:
优点:
Leaf服务可以很方便的线性扩展,性能完全能够支撑大多数业务场景。
ID号码是趋势递增的8byte的64位数字,满足上述数据库存储的主键要求。
容灾性高:Leaf服务内部有号段缓存,即使DB宕机,短时间内Leaf仍能正常对外提供服务。
可以自定义max_id的大小,非常方便业务从原有的ID方式上迁移过来。
缺点:
ID号码不够随机,能够泄露发号数量的信息,不太安全。
DB宕机会造成整个系统不可用。
2 leaf-snowflake方案
2.1Leaf-snowflake方案完全沿用snowflake方案的bit位设计,即是“1+41+10+12”的方式组装ID号。对于workerID的分配,当服务集群数量较小的情况下,完全可以手动配置。Leaf服务规模较大,动手配置成本太高。所以使用Zookeeper持久顺序节点的特性自动对snowflake节点配置wokerID。Leaf-snowflake是按照下面几个步骤启动的:
启动Leaf-snowflake服务,连接Zookeeper,在leaf_forever父节点下检查自己是否已经注册过(是否有该顺序子节点)。
如果有注册过直接取回自己的workerID(zk顺序节点生成的int类型ID号),启动服务。
如果没有注册过,就在该父节点下面创建一个持久顺序节点,创建成功后取回顺序号当做自己的workerID号,启动服务。
2.2对于,时钟回退问题,系统予以了充分的考虑,设计如下逻辑处理。
服务启动时首先检查自己是否写过ZooKeeper leaf_forever节点:
若写过,则用自身系统时间与leaf_forever/${self}节点记录时间做比较,若小于leaf_forever/${self}时间则认为机器时间发生了大步长回拨,服务启动失败并报警。
若未写过,证明是新服务节点,直接创建持久节点leaf_forever/${self}并写入自身系统时间,接下来综合对比其余Leaf节点的系统时间来判断自身系统时间是否准确,具体做法是取leaf_temporary下的所有临时节点(所有运行中的Leaf-snowflake节点)的服务IP:Port,然后通过RPC请求得到所有节点的系统时间,计算sum(time)/nodeSize。
若abs( 系统时间-sum(time)/nodeSize ) < 阈值,认为当前系统时间准确,正常启动服务,同时写临时节点leaf_temporary/${self} 维持租约。
否则认为本机系统时间发生大步长偏移,启动失败并报警。
每隔一段时间(3s)上报自身系统时间写入leaf_forever/${self}。
由于强依赖时钟,对时间的要求比较敏感,在机器工作时NTP同步也会造成秒级别的回退,建议可以直接关闭NTP同步。要么在时钟回拨的时候直接不提供服务直接返回ERROR_CODE,等时钟追上即可。或者做一层重试,然后上报报警系统,更或者是发现有时钟回拨之后自动摘除本身节点并报警

主要对比spark和hadoop的相同点
1 任务调度:任务分配给空闲的机器;尽量将Map任务分配给InputSplit所在的机器,移动计算来减少网络I/O。
2 不同点
直接比较Hadoop和Spark有难度,因为它们处理的许多任务都一样,但是在一些方面又并不相互重叠。
比如说,Spark没有文件管理功能,因而必须依赖Hadoop分布式文件系统(HDFS)或另外某种解决方案。
Hadoop框架的主要模块包括如下:
Hadoop Common
Hadoop分布式文件系统(HDFS)
Hadoop YARN
Hadoop MapReduce
虽然上述四个模块构成了Hadoop的核心,不过还有其他几个模块。这些模块包括:Ambari、Avro、Cassandra、Hive、 Pig、Oozie、Flume和Sqoop,它们进一步增强和扩展了Hadoop的功能。
Spark确实速度很快(最多比Hadoop MapReduce快100倍)。Spark还可以执行批量处理,然而它真正擅长的是处理流工作负载、交互式查询和机器学习。
相比MapReduce基于磁盘的批量处理引擎,Spark赖以成名之处是其数据实时处理功能。Spark与Hadoop及其模块兼容。实际上,在Hadoop的项目页面上,Spark就被列为是一个模块。
Spark有自己的页面,因为虽然它可以通过YARN(另一种资源协调者)在Hadoop集群中运行,但是它也有一种独立模式。它可以作为 Hadoop模块来运行,也可以作为独立解决方案来运行。
MapReduce和Spark的主要区别在于,MapReduce使用持久存储,而Spark使用弹性分布式数据集(RDDS)。

2.1性能
Spark之所以如此快速,原因在于它在内存中处理一切数据。没错,它还可以使用磁盘来处理未全部装入到内存中的数据。
Spark的内存处理为来自多个来源的数据提供了近乎实时分析的功能:营销活动、机器学习、物联网传感器、日志监控、安全分析和社交媒体网站。另 外,MapReduce使用批量处理,其实从来就不是为惊人的速度设计的。它的初衷是不断收集来自网站的信息,不需要这些数据具有实时性或近乎实时性。

2.2易用性
支持Scala(原生语言)、Java、Python和Spark SQL。Spark SQL非常类似于SQL 92,所以几乎不需要经历一番学习,马上可以上手。

Spark还有一种交互模式,那样开发人员和用户都可以获得查询和其他操作的即时反馈。MapReduce没有交互模式,不过有了Hive和Pig等附加模块,采用者使用MapReduce来得容易一点。

2.3成本
“Spark已证明在数据多达PB的情况下也轻松自如。它被用于在数量只有十分之一的机器上,对100TB数据进行排序的速度比Hadoop MapReduce快3倍。”这一成绩让Spark成为2014年Daytona GraySort基准。

2.4兼容性
MapReduce和Spark相互兼容;MapReduce通过JDBC和ODC兼容诸多数据源、文件格式和商业智能工具,Spark具有与MapReduce同样的兼容性。

2.5数据处理
MapReduce是一种批量处理引擎。MapReduce以顺序步骤来操作,先从集群读取数据,然后对数据执行操作,将结果写回到集群,从集群读 取更新后的数据,执行下一个数据操作,将那些结果写回到结果,依次类推。Spark执行类似的操作,不过是在内存中一步执行。它从集群读取数据后,对数据 执行操作,然后写回到集群。
Spark还包括自己的图形计算库GraphX​​。GraphX让用户可以查看与图形和集合同样的数据。用户还可以使用弹性分布式数据集(RDD),改变和联合图形,容错部分作了讨论。

2.6容错
2.6.1 至于容错,MapReduce和Spark从两个不同的方向来解决问题。MapReduce使用TaskTracker节点,它为 JobTracker节点提供了心跳(heartbeat)。如果没有心跳,那么JobTracker节点重新调度所有将执行的操作和正在进行的操作,交 给另一个TaskTracker节点。这种方法在提供容错性方面很有效,可是会大大延长某些操作(即便只有一个故障)的完成时间。
-任务出错是比较常见的,引起错误的原因通常有低质量的代码、数据损坏、节点暂时性故障、一个任务出现下列三种情况的任意一种时被认为出错。
(1)抛出一个没有铺货的异常
(2)以一个非零值退出程序
(3)在一定的事件内没有向Tasktracker报告进度。
当TaskTracker检测到一个错误,TaskTracker将在下一次心跳里向JobTracker报告该错误,JobTracker收到报告的错误后,将会判断是否需要进行重试,如果是,则重新调度该任务。默认的尝试次数为4次,可以通过mapred-site.xml的配置项mapreduce.map.maxattempts配置。该任务可能在集群的任意一个节点重试,这取决于集群资源的利用情况。如果同一个作业的多个任务在同一个TaskTracker节点反复失败,那么JobTracker会将该TaskTracker放到作业级别的黑名单,从而避免将该作业的其他任务分配到该TaskTracker上。如果多个作业的多个任务在同一个TaskTracker节点反复失败,那么JobTracker会将该TaskTracker放到一个全局的黑名单24小时,从而避免所有作业的任务呗分配到TaskTracker上。
当一个任务经过最大尝试数的尝试运行后仍然失败,那么整个作业将被标记为失败。如果我们不希望这样(因为可能作业的溢写结果还是可用的),那么可以设置允许在不处罚整个作业失败的任务失败的最大百分比。
-TaskTracker出错
当TaskTracker进程崩溃或者TaskTracker进程所在节点故障时,JobTracker将接收不到TaskTracker发来的心跳,那么JobTracker将会认为该TaskTracker失效并且在该TaskTracker运行过的任务都会被认为失败,这些将会被重新调度到别的TaskTracker执行,而对于用户来说,在执行MapReduce任务时,只会感觉到该作业在执行的一段时间里变慢了。
-JobTracker出错
在Hadoop中,JobTracker出错是非常严重的额情况,因为在Hadoop中JobTracker存在单节点故障的可能性,所以如果如果JobTracker一旦出错,那么正在运行的所有作业的内部状态信息将会丢失,即使JobTracker马上恢复了,作业的所有任务都会被认为是失败的,即所有作业都需要重新执行。
-HDFS出错
对于依赖底层存储HDFS的作业,一旦HDFS出错,那么对于整个作业来说,还是会执行失败,当DataNode出错时,MapReduce会从其他DataNode上读取所需数据,除非包含任务所需的数据块的节点都出错,否则都是可以恢复的。如果NameNode出错,任务将在下一次访问NameNode时报错,但是MapReduce计算框架还是会尝试执行4次(默认的最大尝试执行次数为4),在这期间,如果NameNode依然处于故障状态,那么作业会最终执行失败。
2.6.2 Spark使用弹性分布式数据集(RDD),它们是容错集合,里面的数据元素可执行并行操作。RDD可以引用外部存储系统中的数据集,比如共享式文件系统、HDFS、HBase,或者提供Hadoop InputFormat的任何数据源。Spark可以用Hadoop支持的任何存储源创建RDD,包括本地文件系统,或前面所列的其中一种文件系统。
RDD拥有五个主要属性:
分区列表
计算每个分片的函数
依赖其他RDD的项目列表
面向键值RDD的分区程序(比如说RDD是散列分区),这是可选属性
计算每个分片的首选位置的列表(比如HDFS文件的数据块位置),这是可选属性
RDD可能具有持久性,以便将数据集缓存在内存中。这样一来,以后的操作大大加快,最多达10倍。Spark的缓存具有容错性,原因在于如果RDD的任何分区丢失,就会使用原始转换,自动重新计算。
分布式数据集的容错性有两种方式:数据检查点记录数据的更新
面向大规模数据分析,数据检查点操作成本很高,需要通过数据中心的网络连接在机器之间复制庞大的数据集,而网络带宽往往比内存带宽低得多,同时还需要消耗更多的存储资源。
因此,Spark选择记录更新的方式。但是,如果更新粒度太细太多,那么记录更新成本也不低。因此,RDD只支持粗粒度转换,即只记录单个块上执行的单个操作,然后将创建RDD的一系列变换序列(每个RDD都包含了他是如何由其他RDD变换过来的以及如何重建某一块数据的信息。因此RDD的容错机制又称“血统(Lineage)”容错)记录下来,以便恢复丢失的分区。Lineage本质上很类似于数据库中的重做日志(Redo Log),只不过这个重做日志粒度很大,是对全局数据做同样的重做进而恢复数据。

2.7可扩展性
按照定义,MapReduce和Spark都可以使用HDFS来扩展。那么,Hadoop集群能变得多大呢?
据称雅虎有一套42000个节点组成的Hadoop集群,可以说扩展无极限。最大的已知Spark集群是8000个节点,不过随着大数据增多,预计集群规模也会随之变大,以便继续满足吞吐量方面的预期。

2.8安全
Hadoop支持Kerberos身份验证,这管理起来有麻烦。然而,第三方厂商让企业组织能够充分利用活动目录Kerberos和LDAP用于身份验证。同样那些第三方厂商还为传输中数据和静态数据提供数据加密。
Hadoop分布式文件系统支持访问控制列表(ACL)和传统的文件权限模式。Hadoop为任务提交中的用户控制提供了服务级授权(Service Level Authorization),这确保客户拥有正确的权限。
Spark的安全性弱一点,目前只支持通过共享密钥(密码验证)的身份验证。Spark在安全方面带来的好处是,如果你在HDFS上运行Spark,它可以使用HDFS ACL和文件级权限。此外,Spark可以在YARN上运行,因而能够使用Kerberos身份验证。

总结
Spark与MapReduce是一种相互共生的关系。Hadoop提供了Spark所没有的功能特性,比如分布式文件系统,而Spark 为需要它的那些数据集提供了实时内存处理。完美的大数据场景正是设计人员当初预想的那样:让Hadoop和Spark在同一个团队里面协同运行。

LMAX Disruptor交易系统吞吐量快的关键原因。为什么我们要将其开源?我们意识到对高性能编程领域的一些传统观点有点不对劲。
尽管ringbuffer是整个模式(Disruptor)的核心,但是Disruptor对ringbuffer的访问控制策略才是真正的关键点所在。
1.首先介绍ringbuffer
1.1ringbuffer是一个环,换上的每个元素都有一个序号,序号随着元素的写入而递增(一直增加,绕过环以后继续增加),底层使用数组存储。它与常见队列的区别是:它不删除元素而是覆盖元素;他只有头指针没有尾指针。
1.1它比链表快,容易预测的访问模式(数组元素在内存连续存储);它缓存命中率高,因为根据CPU的缓存加载原则,会将元素附近64字节的内容一次性加载到缓存,这样相邻元素被加载到缓存后,下次访问则可以直接在缓存中取得。
1.2预先为数组分配内存,使得数组对象一直存在,避免了垃圾回收,节省了时间。不需要像链表那样,增删节点时都有内存操作,产生垃圾回收现象。
2.ConsummerBarrier与消费者
2.1 ComsummerBarrier由RingBuffer创建,代表消费者与RingBuffer进行交互。消费者调用Barrier.waitFor,传递它需要的下一个序号,比如9。消费者相当于告诉Barrier:你打到的数字比我这个要大的时候请告诉我。
final long availableSeq = consumerBarrier.waitFor(nextSequence);
当数据写入后,消费者会收到来自Barrier的通知。实际消费者是通过waitStrategy来获得通知的。通知9,10,11,12都已写入。现在序号12到了,消费者通过Barrier获得批量数据。
当多个消费者同时消费时,不需要锁机制。
3.ProducerBarrier与生产者
ProducerBarrier作为Producer与RingBuffer交流的媒介。同时,为了减轻Ringbuffer的复杂度,ProducerBarrier负责记录消费者分别消费到的序列号,所以ProducerBarrier也直接与Consumer通信。
3.1ProducerBarrier作为既知道Consumer消费到的序列号,又知道生产者想写入的序列号。它在整个Disruptor的访问控制上起到了十分关键的作用。由于每个消费者消费速率不同,有的快有的慢。会出现生产者要写入的节点位置,刚好还在被消费者消费,此时则需要等待。当消费者批量消费完以后,会通知ProducerBarrier,然后它则取到之前等待的节点Entry,然后交给Producer进行写入。Producer写入后要求ProducerBarrier进行提交。ProducerBarrier更新Ringbuffer上的游标到刚才更新的节点的序号。最后Producer通知Consumer的waitStrategy。
但是,要补充一下,其实整个对提交的管理是ProducerBarrier委托ClaimStrategy来做的。
ProducerBarrier通过ClaimStrategy来获取RingBuffer的游标。而且它还负责给Producer分发序号,以及追踪哪些序号被使用。在commit的时候,其实ClaimStrategy要等待游标到达待提交节点的上一个节点,才能进行提交。比如:游标在12,Prodcuer在14上先提取请求,此时并不能commit,需要等到13提交请求,然后游标更新到13,然后14才能commit。你会看到,尽管生产者在不同的时间完成数据写入,但是 Ring Buffer 的内容顺序总是会遵循 nextEntry() 的初始调用顺序。也就是说,如果一个生产者在写入 Ring Buffer 的时候暂停了,只有当它解除暂停后,其他等待中的提交才会立即执行
3.2ProducerBarrier上的批处理
因为它知道当前RingBuffer游标的位置,也知道各个消费者的消费到的位置,所以就知道哪些节点是可以写入的。

  • 路由:将原SQL转换成在真实数据库真实表中可执行SQL的过程。例如:
    SELECT * FROM t_order WHERE order_id IN (1, 2);
  • 路由后成为
    SELECT * FROM t_order_0 WHERE order_id IN (1, 2);
    SELECT * FROM t_order_1 WHERE order_id IN (1, 2);
  • 分片路由
  • 标准路由
    • 单片路由:原SQL分片键的操作符是等号,映射后只有一条可执行SQL。例如:
      SELECT * FROM t_order WHERE order_id=2;
      路由后成为
      SELECT * FROM t_order_0 WHERE order_id=2;
    • 多片路由:原SQL分片键的操作符是IN,映射后有多条可执行SQL。例如:
      SELECT * FROM t_order WHERE order_id IN (1, 2);
      路由后成为
      SELECT * FROM t_order_0 WHERE order_id IN (1, 2);
      SELECT * FROM t_order_1 WHERE order_id IN (1, 2);
    • 范围路由:原SQL分片键的操作符是BETWEEN,映射后有多条可执行SQL。
      SELECT * FROM t_order WHERE order_id BETWEEN (1, 12);
      路由后成为
      SELECT * FROM t_order_0 WHERE order_id BETWEEN (1, 12);
      SELECT * FROM t_order_1 WHERE order_id BETWEEN (1, 12);
    • 带有绑定表的多片路由:绑定表指分片规则一致的主表和子表。例如:t_order表和t_order_item表,均按照order_id分片,则此两张表互为绑定表关系。绑定表之间的多表关联查询不会出现笛卡尔积关联,关联查询效率将大大提升。
      SELECT * FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id WHERE order_id IN (1, 2);
      路由后成为
      SELECT * FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE order_id IN (1, 2);
      SELECT * FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE order_id IN (1, 2);
  • 直接路由
    • Hint路由
      它需要通过Hint(使用HintAPI直接指定路由至库表)方式分片,并且是只分库不分表的前提下,则可以避免SQL解析和之后的结果归并。直接路由还可以用于分片键不在SQL中的场景。
      例如,设置用于数据库分片的键为3hintManager.setDatabaseShardingValue(3)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      String sql = "SELECT * FROM t_order";
      try (HintManager hintManager = HintManager.getInstance();
      Connection conn = dataSource.getConnection();
      PreparedStatement preparedStatement = conn.prepareStatement(sql)) {
      hintManager.setDatabaseShardingValue(3);
      try (ResultSet rs = preparedStatement.executeQuery()) {
      while (rs.next()) {
      //...
      }
      }
      }
      路由算法为’value%2‘。当一个逻辑库order_database对应2个真实库order_database_0,order_databse_1时,路由后sql将在order_databse_1上执行。
  • 笛卡尔路由
    是指在关联查询中,各个表的分片键不同,没有绑定表。导致转换后的可执行SQL时,真实表之间需要进行笛卡尔积组合生成大量SQL。例如:
    假设分片键order_id将数值10路由至第0片,将数值11路由至第1片,那么路由后的SQL应该为4条,它们呈现为笛卡尔积:
    SELECT i.* FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
    路由后成为
    SELECT i.* FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
    SELECT i.* FROM t_order_0 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
    SELECT i.* FROM t_order_1 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
    SELECT i.* FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
  • 广播路由
  • 全库路由
    用于处理对数据库的操作,包括用于库设置的SET类型的数据库管理命令,以及TCL这样的事务控制语句。在这种情况下,会根据逻辑库的名字遍历所有符合名字匹配的真实库,并在真实库中执行该命令。
    例如 set autocommit=0
    在order_database中执行,order_database有两个真实库。则实际会在order_database_0,order_database_1上都执行这个命令。
  • 全库表路由
    用于处理对数据库中与其逻辑表相关的所有真实表的操作,主要包括不带分片键的DQL和DML,以及DDL等
    例如 SELECT * FROM t_order WHERE good_prority IN (1, 10);
    则会遍历所有数据库中的所有表,逐一匹配逻辑表和真实表名,能够匹配得上则执行。
    路由后成为
    SELECT * FROM t_order_0 WHERE good_prority IN (1, 10);
    SELECT * FROM t_order_1 WHERE good_prority IN (1, 10);
    SELECT * FROM t_order_2 WHERE good_prority IN (1, 10);
    SELECT * FROM t_order_3 WHERE good_prority IN (1, 10);
  • 全实例路由
    用于DCL操作,授权语句针对的是数据库的实例。无论一个实例中包含多少个Schema,每个数据库的实例执行一次。
    例如:
    create user customer@10.98.17.138 identified by '123';
    这个命令将在所有的真实数据库实例中执行,以确保customer用户可以访问每一个实例。
  • 单播路由
    用于获取某一真实表信息的场景,它仅需要从任意库中的任意真实表中获取数据即可。
    例如:
    DESCRIBE t_order;
    t_order的两个真实表t_order_0,t_order_1的描述结构相同,所以这个命令在任意真实表上选择执行一次。
  • 阻断路由
    用于屏蔽SQL对数据库的操作。
    例如:USE order_db;
    这个命令不会在真实数据库中执行,因为ShardingSphere采用的是逻辑Schema的方式,无需将切换数据库Schema的命令发送至数据库中。

1.高并发

1.1缓存

缓存可以减少并发对后台的压力,尽可能地在前面的层级处理请求。缓存按照从前到后的顺序,存在于如下的网元中。

http客户端->CDN->Nginx->分布式缓存->应用->分布式缓存->数据库。

http客户端缓存:HTTP缓存指我们用浏览器访问网站时,根据服务器返回的HTTP缓存响应头设置缓存相应的数据。

缓存有两种使用方式:1访问就可以直接使用(LastModifty+Expires方式设置过期时间)。2去服务器验证数据是否过期(Cache-Control+Etag),没过期则返回304。

CDN缓存:静态资源存放在CDN,客户端访问时先访问CDN。如果是第一次访问没有存储用户的所需要内容,CDN则向后台业务发起请求并缓存结果。

Nginx缓存:当请求到达Nginx后,如果缓存中有结果,则直接返回请求。如果没有,则向后台应用请求,并缓存结果。

应用:应用本地可以使用堆缓存(如:guava)和堆外缓存(Encache)。在微服务中Hystrix,可以做结果缓存和请求合并。

分布式缓存:既可以被Nginx作为缓存使用,也可以被应用当作缓存使用。例如:redis集群。

数据库:数据库缓存。mysql缓存机制就是缓存sql 文本及缓存结果,用KV形式保存再服务器内存中,如果运行相同的sql,服务器直接从缓存中去获取结果,不需要在再去解析、优化、执行sql。 如果这个表修改了,那么使用这个表中的所有缓存将不再有效,查询缓存值得相关条目将被清空。显然,者对于频繁更新的表,查询缓存不合适,对于一些不变的数据且有大量相同sql查询的表,查询缓存会节省很大的性能。

1.2扩容

应用扩容:垂直扩容(增加单体物理主机能力),水平扩容(集群),应用拆分(微服务化)。

数据库扩容:分库分表,读写分离。

1.3队列

异步处理:通过异步处理,可以提升主流程响应速度,而非主流程、非重要处理可以集中处理,这样还可以将任务聚合批量处理。

系统解耦:比如用户成功支付完成后,需要通知生产配货系统、发票系统、库存系统、推荐系统、搜索系统等进行业务处理。这些业务不需要实时处理、不需要强一致性,只需要保证最终一致性即可,因此,可以通过消息队列、任务队列进行系统解耦。

数据同步:比如把MySql变更的数据同步到Redis,或者将MySQL的数据同步到Mongodb,或者让机房之间的数据同步,或者主从数据同步等,此时可以

考虑使用databus,canal、otter等。使用数据总线队列进行数据同步的好处是可以保证数据修改的有序性。

流量削峰:系统瓶颈一般在数据库上,比如扣减库存、下单等。此时可以考虑使用队列将变更请求暂时放入队列,通过缓存+队列暂存的方式将数据库流量削峰。

在秒杀系统中,下单是系统的瓶颈,可以使用队列进行排队和限流,从而保护下单服务,通过队列暂存或者队列限流进行流量削峰。

1.4异步

CompletableFuture:它可以实现应用内部,线程间的异步模式,而且是异步非阻塞模式。

2.高可用

2.1集群

使用集群模式来保持同一个业务的不间断服务,是高可用的常见形式。使用nginx做负载均衡和反向代理。

2.2隔离

隔离保证,被隔离对象之间不会相互影响,某个出问题时,其他可以正常工作。

包括:线程隔离、进程隔离、集群隔离、机房隔离、读写隔离、动静隔离(将静态资源放在CDN上,将动态资源由NGINX处理)、热点隔离(将大访问量的业务单独抽离出来,比如秒杀)、Hystrix。

2.3限流

2.3.1接入层限流

令牌桶算法:保证请求进入速度,但是流出不限制,有可能造成有小规模并发。

漏斗算法:保证请求流出的速度。

2.3.2应用级限流

限流总并发、连接、请求数:通过设置tomcat的connector参数实现

限流总资源数:可以使用池化技术来限制总资源数,如:连接池、线程池。

限流某个接口的总并发数、请求数:Hystrix在信号量模式下也使用Semaphore限制某个接口的总并发数。

限流某个接口的时间窗请求数:可以使用Guava的Cache来存储计数器。

平滑限流某个接口的请求数:可以使用guava RateLimiter提供的令牌桶算法

2.4降级

页面降级:在大促或者某些特殊情况下,某些页面占用了一些稀缺服务资源,在紧急情况下可以对其整个降级,以达到丢足保帅的目的。

页面片段降级:比如,商品详情页中的商家部分因为数据错误,此时,需要对其进行降级。

页面异步请求降级:比如,商品详情页上有推荐信息/配送至等异步加载的请求,如果这些信息响应慢或者后端服务有问题,则可以进行降级。

服务功能降级:比如,渲染商品详情页时,需要调用一些不太重要的服务,而这些服务在异常情况下直接不获取,降级即可。

读降级:比如,多级缓存模式,如果后端服务有问题,则可以降级为只读缓存,这种方式适用于对读一致性要求不高的场景。

写降级:比如,秒杀抢购,我们可以只进行Cache的更新,然后一步减少库存到DB,保证最终一致性即可,此时可以将DB降级为Cache。

爬虫降级:在大促活动时,可以将爬虫流量导向静态页或者返回空数据,从而保护后端稀缺资源。

风控降级:如抢购,秒杀等业务,完全可以识别机器人,用户画像或者根据用户风控级别进行降级处理,直接拒绝高风险用户。

2.5超时机制

超时之后应该有响应的策略来处理。常见的策略有重试(等一会儿再试,尝试其他分组服务,尝试其他机房服务,重试算法可考虑使用如指数退避算法),摘掉不存活节点(负载均衡/分布式缓存场景下),托底(返回历史数据/静态数据/缓存数据),等待页或者错误页。对于非幂等写服务应避免重试,或者可以考虑提前生成唯一流水号来保证写服务操作通过判断流水号来实现幂等操作。在进行数据库/缓存服务器操作时,记得经常检查慢查询,慢查询通常是引起服务出问题的罪魁祸首。也要考虑在超时严重时,直接将该服务加攻击,待该服务修复后再取消降级。对于负载均衡的中间件,请考虑配置心跳/存活检查,而不是惰性检查。

超时重试必然导致请求响应时间增加,最坏情况下的响应时间=重试次数*单次超时时间,这很可能严重影响用户体验。导致用户不断刷新页面来重复请求,最后导致服务接受的请求太多而挂掉,因此除了控制单次超时时间,也要控制好用户能忍受的最长超时时间。
超时时间太短会导致服务调用成功率降低,超时时间太长又会导致本应成功的调用却失败了,这也要根据实际场景来选择最适合当前业务的超时时间。甚至是程序动态计算超时时间,比如商品详情页的库存状态服务,可以设置较短的超时时间。当超时时降级返回有货,而结算页服务就需要设置稍微长一些的超时时间保证确实有货。

在实际开发中,不要轻视超时时间,很多重大事故都是因为超时时间不合理导致的,设置超时时间一定是只有好处没有坏处的。

2.6回滚机制

指在事务过程中,出现错误而进行的回滚。分为单机数据库事务回滚和分布式事务回滚。