作者: pbdatacn

  • JVM 性能调优工具学习

    一、What

    JDK 本身提供了很多很方便的 JVM 性能调优工具,如 VisualVM、Jconsole、jps、jstack、jmap、jhat、Jstat、jprof等。

    可以帮我们解决下列问题:

    • OutofMemory 内存不足
    • 内存泄漏
    • 线程死锁
    • 锁争用(Lock Contention)
    • java 进程消耗 CPU 过高

    1.   Jstack(查看线程)

    1.1 作用:

    jstack主要用来抓取 Java JVM 中 某个进程 某一时刻 线程堆栈信息。其实就是抓取 thread dump 文件。

        thread dump 诊断 java 应用问题的工具,可以显示 Java JVM中的所有线程在某一个时间点的快照文件

    1.2 内容:

    线程标识,运行状态,调用的堆栈、调用的堆栈包含完整的类名、所执行的方法、还有源代码的行数。

    Sun JVM 常见线程状态:

    Runnable(R): 当前可以运行的线程。

    Waiting on monitor(CW): 线程主动 wait

    Waiting for monitor entry(MW):线程等锁

    JVM 中的 thin lock, fat lock, spin lock, tasuki lock:

    1.3 Demo:

    “process reaper” daemon prio=10 tid=0x00007f3d7014b800 nid=0x8b80a waiting on condition [0x00007f3dc68f0000]

    java.lang.Thread.State: TIMED_WAITING (parking)

    at sun.misc.Unsafe.park(Native Method)

    – parking to wait for  <0x00000000f007cfb0> (a java.util.concurrent.SynchronousQueue$TransferStack)

    at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:226)

    at java.util.concurrent.SynchronousQueue$TransferStack.awaitFulfill(SynchronousQueue.java:460)

    at java.util.concurrent.SynchronousQueue$TransferStack.transfer(SynchronousQueue.java:359)

    at java.util.concurrent.SynchronousQueue.poll(SynchronousQueue.java:942)

    at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1068)

    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)

    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)

    at java.lang.Thread.run(Thread.java:745)

     

    process reaper: 线程名称

    daemon: 线程类型,守护线程

    prio: 线程优先级

    tid: thread id,JVM 中线程唯一标识

    nid: native thread id,系统中线程唯一标识

    waiting on condition: 线程状态

    at…: 堆栈信息

     

    Jstack 的目的主要是抓取 thread dump 文件然后使用工具进行分析。

    1.4 常见线程:

        Attach Listener负责接收外部命令,用户第一次执行 JVM 命令的时候得到启动。

        Signal DispatherAttach Listener 接收到外部命令交给 Signal Dispather 进行分发到各个不同的模块处理,并且返回处理结果。

        CompilerThread0实时编译卸载 class,JVM 会启动多个线程来处理这部分工作,线程名称后面的数字也会累加。

        Concurrent Mark-Sweep GC Thread并发标记清除垃圾回收器线程(CMC GC),该线程主要针对老年代垃圾回收

        Finalizer线程:垃圾收集前,调用对象的finalize()方法

     

    • 只有当开始一轮垃圾收集时,才会开始调用finalize()方法;因此并不是所有对象的finalize()方法都会被执行
    • 该线程也是daemon线程,因此如果虚拟机中没有其他非daemon线程,不管该线程有没有执行完finalize()方法,JVM也会退出
    • JVM在垃圾收集时会将失去引用的对象包装成Finalizer对象(Reference的实现),并放入ReferenceQueue,由Finalizer线程来处理;最后将该Finalizer对象的引用置为null,由垃圾收集器来回收
    • JVM为什么要单独用一个线程来执行finalize()方法呢?如果JVM的垃圾收集线程自己来做,很有可能由于在finalize()方法中误操作导致GC线程停止或不可控,这对GC线程来说是一种灾难

    2.     Jstat(查看性能)

    2.1 作用:

    可以观察到classloader,compiler,gc相关信息。可以实时监控资源和性能 。

    2.2 命令选项:

    -class统计class loader行为信息
    -compile统计编译行为信息
    -gc统计jdk gc时heap信息
    -gccapacity: 统计不同的generations(不知道怎么翻译好,包括新生区,老年区,permanent区)相应的heap容量情况
    -gccause统计gc的情况,(同-gcutil)和引起gc的事件
    -gcnew统计gc时,新生代的情况
    -gcnewcapacity统计gc时,新生代heap容量
    -gcold统计gc时,老年区的情况
    -gcoldcapacity统计gc时,老年区heap容量
    -gcpermcapacity统计gc时,permanent区heap容量
    -gcutil统计gc时,heap情况

    2.3 参数内容

    S0  — Heap上的 Survivor space 0 区已使用空间的百分比
    S0C:S0当前容量的大小
    S0U:S0已经使用的大小
    S1  — Heap上的 Survivor space 1 区已使用空间的百分比
    S1C:S1当前容量的大小
    S1U:S1已经使用的大小
    E   — Heap上的 Eden space 区已使用空间的百分比
    EC:Eden space当前容量的大小
    EU:Eden space已经使用的大小
    O   — Heap上的 Old space 区已使用空间的百分比
    OC:Old space当前容量的大小
    OU:Old space已经使用的大小
    P   — Perm space 区已使用空间的百分比
    OC:Perm space当前容量的大小
    OU:Perm space已经使用的大小
    YGC — 从应用程序启动到采样时发生 Young GC 的次数
    YGCT– 从应用程序启动到采样时 Young GC 所用的时间(单位秒)
    FGC — 从应用程序启动到采样时发生 Full GC 的次数
    FGCT– 从应用程序启动到采样时 Full GC 所用的时间(单位秒)
    GCT — 从应用程序启动到采样时用于垃圾回收的总时间(单位秒),它的值等于YGC+FGC

    3.     Jmap(查看内存)

    3.1 作用:

    jmap 用来查看堆内存使用状况,一般结合 jhat 使用

    3.2 命令选项

    -heap打印 heap 的概要信息,GC 使用的算法,heap 的配置以及 wise heap 的使用情况。

    -histo打印每个 class 的实例数目,内存占用,类全名信息,

    -permstat打印classload和jvm heap长久层的信息. 包含每个classloader的名字,活泼性,地址,父classloader和加载的class数量. 另外,内部String的数量和占用内存数也会打印出来.

    二、Why

    有时候内存泄漏,内存溢出,或者死锁等现象出现的时候仅仅通过应用程序的日志信息是很难定位问题的,很多时候这种情况出现的时候我们的解决方案都是重启服务器,但是这种方式并没有从根本上解决问题,程序运行一段时间还是会出现上述现象。

    所以为了从根本上解决这些问题,我们首先要定位问题,知道问题发生的原因,因为 Java 应用程序都是运行在 JVM 上的,所以了解 JVM 的概念和原理是我们发现问题的第一步。

    JVM 中为我们提供了一些内置的工具可以用于抓取 JVM 的状态信息,比如使用 Jstack 可以抓取当前进程的堆栈信息,分析后判断是否存在死锁问题,使用 Jmap 可以抓取堆内存信息,分析后可以发现各种内存泄漏,内存溢出的原因,使用 Jstat 可以查看系统的性能等。

    三、How

    1. Jstack

    操作流程:

    第一步:抓取堆栈信息写入 a.txt

    jstack –l <pid>

     

    第二步:使用工具分析堆栈信息

    2. Jstat

    抓取信息:

    # jstat -gc 30077 100 5

    3. Jmap

    使用 IBM 的HeapAnalyzer 分析堆内存信息。

    第一步:抓取数据

     

    第二步:使用工具分析:HeapAnalyzer

     

    四、参考资料

    【1】性能分析之– JAVA Thread Dump 分析综述

    http://blog.csdn.net/rachel_luo/article/details/8920596

    【2】jstack(查看线程)、jmap(查看内存)和jstat(性能分析)命令

    http://guafei.iteye.com/blog/1815222

    【3】三个实例演示 Java Thread Dump 日志分析

    http://www.cnblogs.com/zhengyun_ustc/archive/2013/01/06/dumpanalysis.html

    【4】关于JVM的Thin Lock, Fat Lock, SPIN Lock与Tasuki Lock

    http://www.blogjava.net/security/archive/2009/02/16/jvm_thin-lock_fat-lock__spin-lock_tasuki-lock.html

    【5】JVM性能调优监控工具

    https://my.oschina.net/feichexia/blog/196575

  • Spark机器学习数据流水线

    关键点:

    • 了解机器学习数据流水线有关内容。
    • 怎么用Apache Spark机器学习包来实现机器学习数据流水线。
    • 数据价值链处理的步骤。
    • Spark机器学习流水线模块和API。
    • 文字分类和广告检测用例。

    引用:http://www.infoq.com/cn/articles/apache-sparkml-data-pipelines

     

    在之前的“用Apache Spark做大数据处理”系列文章中,我们学习了Apache Spark框架,介绍了Spark和它用作大数据处理的不同库(第一部分),Spark SQL库(第二部分),Spark流(第三部分)和Spark MLlib机器学习库(第四部分)。

    在这篇文章中,我们Spark的其它机器学习API,名为Spark ML,如果要用数据流水线来开发大数据应用程序的话,这个是推荐的解决方案。

    Spark ML(spark.ml)包提供了构建在DataFrame之上的机器学习API,它已经成了Spark SQL库的核心部分。这个包可以用于开发和管理机器学习流水线。它也可以提供特征抽取器、转换器、选择器,并支持分类、汇聚和分簇等机器学习技术。这些全都对开发机器学习解决方案至关重要。

    在这里我们看看如何使用Apache Spark来做探索式数据分析(Exploratory Data Analysis)、开发机器学习流水线,并使用Spark ML包中提供的API和算法。

    因为支持构建机器学习数据流水线,Apache Spark框架现在已经成了一个非常不错的选择,可以用于构建一个全面的用例,包括ETL、指量分析、实时流分析、机器学习、图处理和可视化等。

    机器学习数据流水线

    机器学习流水线可以用于创建、调节和检验机器学习工作流程序等。机器学习流水线可以帮助我们更加专注于项目中的大数据需求和机器学习任务等,而不是把时间和精力花在基础设施和分布式计算领域上。它也可以在处理机器学习问题时帮助我们,在探索阶段我们要开发迭代式功能和组合模型。

    机器学习工作流通常需要包括一系列的处理和学习阶段。机器学习数据流水线常被描述为一种阶段的序列,每个阶段或者是一个转换器模块,或者是个估计器模块。这些阶段会按顺序执行,输入数据在流水线中流经每个阶段时会被处理和转换。

    机器学习开发框架要支持分布式计算,并作为组装流水线模块的工具。还有一些其它的构建数据流水线的需求,包括容错、资源管理、可扩展性和可维护性等。

    在真实项目中,机器学习工作流解决方案也包括模型导入导出工具、交叉验证来选择参数、为多个数据源积累数据等。它们也提供了一些像功能抽取、选择和统计等的数据工具。这些框架支持机器学习流水线持久化来保存和导入机器学习模型和流水线,以备将来使用。

    机器学习工作流的概念和工作流处理器的组合已经在多种不同系统中越来越受欢迎。象scikit-learnGraphLab等大数据处理框架也使用流水线的概念来构建系统。

    一个典型的数据价值链流程包括如下步骤:

    • 发现
    • 注入
    • 处理
    • 保存
    • 整合
    • 分析
    • 展示

    机器学习数据流水线所用的方法都是类似的。下图展示了在机器学习流水线处理中涉及到的不同步骤。

    步骤#

    名字

    描述

    ML1

    数据注入

    从不同的数据源中导入数据。

    ML2

    数据清洗

    对数据进行预处理,为接下来的机器学习数据分析做好准备。

    ML3

    功能抽取

    也叫特征工程,这一步是从数据集中抽取功能。

    ML4

    模型训练

    在接下来的几个步骤里用训练数据集来训练机器学习模型。

    ML5

    模型验证

    接下来要基于不同的预测参数来评估机器学习模型的效率。我们也会在验证步骤调节模型,这一步用于挑选出最佳模型。

    ML6

    模型测试

    这一步是在做模型部署之前进行测试。

    ML7

    模型部署

    最后一步是把选出来的模型部署到生产环境中运行。

    表一:机器学习流水线处理步骤

    这些步骤也可以用下面的图一表示。

    图一:机器学习数据流水线处理流图

    接下来让我们一起看看每个步骤的细节。

    数据注入:我们收集起来供给机器学习流水线应用程序的数据可以来自于多种数据源,数据规模也是从几百GB到几TB都可以。而且,大数据应用程序还有一个特征,就是注入不同格式的数据。

    数据清洗:数据清洗这一步在整个数据分析流水线中是第一步,也是至关重要的一步,也可以叫做数据清理或数据转换,这一步主要是要把输入数据变成结构化的,以方便后续的数据处理和预测性分析。依进入到系统中的数据质量不同,总处理时间的60%-70%会被花在数据清洗上,把数据转成合适的格式,这样才能把机器学习模型应用到数据上。

    数据总会有各种各样的质量问题,比如数据不完整,或者数据项不正确或不合法等。数据清洗过程通常会使用各种不同的方法,包括定制转换器等,用流水线中的定制的转换器去执行数据清洗动作。

    稀疏或粗粒数据是数据分析中的另一个挑战。在这方面总会发生许多极端案例,所以我们要用上面讲到的数据清洗技术来保证输入到数据流水线中的数据必须是高质量的。

    伴随着我们对问题的深入理解,每一次的连续尝试和不断地更新模型,数据清洗也通常是个迭代的过程。象TrifactaOpenRefineActiveClean等数据转换工具都可以用来完成数据清洗任务。

    特征抽取:在特征抽取(有时候也叫特征工程)这一步,我们会用特征哈希(Hashing Term Frequency)和Word2Vec等技术来从原始数据中抽取具体的功能。这一步的输出结果常常也包括一个汇编模块,会一起传入下一个步骤进行处理。

    模型训练:机器学习模型训练包括提供一个算法,并提供一些训练数据让模型可以学习。学习算法会从训练数据中发现模式,并生成输出模型。

    模型验证:这一步包评估和调整机器学习模型,以衡量用它来做预测的有效性。如这篇文章所说,对于二进制分类模型评估指标可以用接收者操作特征(Receiver Operating Characteristic,ROC)曲线。ROC曲线可以表现一个二进制分类器系统的性能。创建它的方法是在不同的阈值设置下描绘真阳性率(True Positive Rate,TPR)和假阳性率(False Positive Rate,FPR)之间的对应关系。

    模型选择:模型选择指让转换器和估计器用数据去选择参数。这在机器学习流水线处理过程中也是关键的一步。ParamGridBuilder和CrossValidator等类都提供了API来选择机器学习模型。

    模型部署:一旦选好了正确的模型,我们就可以开始部署,输入新数据并得到预测性的分析结果。我们也可以把机器学习模型部署成网页服务

    Spark机器学习

    机器学习流水线API是在Apache Spark框架1.2版中引入的。它给开发者们提供了API来创建并执行复杂的机器学习工作流。流水线API的目标是通过为不同机器学习概念提供标准化的API,来让用户可以快速并轻松地组建并配置可行的分布式机器学习流水线。流水线API包含在org.apache.spark.ml包中。

    Spark ML也有助于把多种机器学习算法组合到一条流水线中。

    Spark机器学习API被分成了两个包,分别是spark.mllib和spark.ml。其中spark.ml包包括了基于RDD构建的原始API。而spark.ml包则提供了构建于DataFrame之上的高级API,用于构建机器学习流水线。

    基于RDD的MLlib库API现在处于维护模式

    如下面图二所示,Spark ML是Apache Spark生态系统中的一个非常重要的大数据分析库。

    图二:包括了Spark ML的Spark生态系统

    机器学习流水线模块

    机器学习数据流水线包括了完成数据分析任务所需要的多个模块。数据流水线的关键模块被列在了下面:

    • 数据集
    • 流水线
    • 流水线的阶段
    • 转换器
    • 估计器
    • 评估器
    • 参数(和参数地图)

    接下来我们简单看看这些模块可以怎么对应到整体的步骤中。

    数据集:在机器学习流水线中是使用DataFrame来表现数据集的。它也允许按有名字的字段保存结构化数据。这些字段可以用于保存文字、功能向量、真实标签和预测。

    流水线:机器学习工作流被建模为流水线,这包括了一系列的阶段。每个阶段都对输入数据进行处理,为下一个阶段产生输出数据。一个流水线把多个转换器和估计器串连起来,描述一个机器学习工作流。

    流水线的阶段:我们定义两种阶段,转换器和估计器。

    转换器:算法可以把一个DataFrame转换成另一个DataFrame。比如,机器学习模型就是一个转换器,用于把一个有特征的DataFrame转换成一个有预测信息的DataFrame。

    转换器会把一个DataFrame转成另一个DataFrame,同时为它加入新的特征。比如在Spark ML包中,OneHotEncoder就会把一个有标签索引的字段转换成一个有向量特征的字段。每个转换器都有一个transform()函数,被调用时就会把一个DataFrame转换成另一个。

    估计器:估计器就是一种机器学习算法,会从你提供的数据中进行学习。估计器的输入是一个DataFrame,输出就是一个转换器。估计器用于训练模型,它生成转换器。比如,逻辑回归估计器就会产生逻辑回归转换器。另一个例子是把K-Means做为估计器,它接受训练数据,生成K-Means模型,就是一个转换器。

    参数:机器学习模块会使用通用的API来描述参数。参数的例子之一就是模型要使用的最大迭代次数。

    下图展示的是一个用作文字分类的数据流水线的各个模块。

    图三:使用Spark ML的数据流水线

    用例

    机器学习流水线的用例之一就是文字分类。这种用例通常包括如下步骤:

    • 清洗文字数据
    • 将数据转化成特征向量,并且
    • 训练分类模型

    在文字分类中,在进行分类模型(类似SVM)的训练之前,会进行n-gram抽象和TF-IDF特征权重等数据预处理。

    另一个机器学习流水线用例就是在这篇文章中描述的图像分类。

    还有很多种其它机器学习用例,包括欺诈检测(使用分类模型,这也是监督式学习的一部分),用户分区(聚簇模型,这也是非监督式学习的一部分)。

    TF-IDF

    词频-逆向文档频率(Term Frequency – Inverse Document Frequency,TF-IDF)是一种在给定样本集合内评估一个词的重要程度的静态评估方法。这是一种信息获取算法,用于在一个文档集合内给一个词的重要性打分。

    TF:如果一个词在一份文档中反复出现,那这个词就比较重要。具体计算方法为:

    TF = (# of times word X appears in a document) / (Total # of
    words in the document)
    

    IDF:但如果一个词在多份文档中都频繁出现(比如the,and,of等),那就说明这个词没有什么实际意义,因此就要降低它的评分。

    示例程序

    下面我们看个示例程序,了解一下Spark ML包可以怎样用在大数据处理系统中。我们会开发一个文档分类程序,用于区别程序输入数据中的广告内容。测试用的输入数据集包括文档、电子邮件或其它任何从外部系统中收到的可能包含广告的内容。

    我们将使用在Strata Hadoop World Conference研讨会上讨论的“用Spark构建机器学习应用”的广告检测示例来构建我们的示例程序。

    用例

    这个用例会对发送到我们的系统中的各种不同消息进行分析。有些消息里面是含有广告信息的,但有些消息里面没有。我们的目标就是要用Spark ML API找出那些包含了广告的消息。

    算法

    我们将使用机器学习中的逻辑回归算法。逻辑回归是一种回归分析模型,可以基于一个或多个独立变量来预测得到是或非的可能结果。

    详细的解决方案

    接下来咱们看看这个Spark ML示例程序的细节,以及运行步骤。

    数据注入:我们会把包含广告的数据(文本文件)和不包含广告的数据都导入。

    数据清洗:在示例程序中,我们不做任何特别的数据清洗操作。我们只是把所有的数据都汇聚到一个DataFrame对象中。

    我们随机地从训练数据和测试数据中选择一些数据,创建一个数组对象。在这个例子中我们的选择是70%的训练数据,和30%的测试数据。

    在后续的流水线操作中我们分别用这两个数据对象来训练模型和做预测。

    我们的机器学习数据流水线包括四步:

    • Tokenizer
    • HashingTF
    • IDF
    • LR

    创建一个流水线对象,并且在流水线中设置上面的各个阶段。然后我们就可以按照例子,基于训练数据来创建一个逻辑回归模型。

    现在,我们再使用测试数据(新数据集)来用模型做预测。

    下面图四中展示了例子程序的架构图。

    图4:数据分类程序架构图

    技术

    在实现机器学习流水线解决方案时我们用到了下面的技术。

    技术

    版本

    Apache Spark

    2.0.0

    JDK

    1.8

    Maven

    3.3

    表二:在机器学习例子中用到的技术和工具

    Spark ML程序

    根据研讨会上的例子而写成的机器学习代码是用Scala编程语言写的,我们可以直接使用Spark Shell控制台来运行这个程序。

    广告检测Scala代码片段:

    第一步:创建一个定制的类,用来存储广告内容的细节。

    case class SpamDocument(file: String, text: String, label:
    Double)
    

    第二步:初始化SQLContext,并通过隐式转换方法来把Scala对象转换成DataFrame。然后从存放着输入文件的指定目录导入数据集,结果会返回RDD对象。然后由这两个数据集的RDD对象创建DataFrame对象。

    val sqlContext = new SQLContext(sc)
    import sqlContext.implicits._
    
    //
    // Load the data files with spam
    //
    val rddSData = sc.wholeTextFiles("SPAM_DATA_FILE_DIR", 1)
    val dfSData = rddSData.map(d => SpamDocument(d._1, d._2,1)).toDF()
    dfSData.show()
    
    //
    // Load the data files with no spam
    //
    val rddNSData = sc.wholeTextFiles("NO_SPAM_DATA_FILE_DIR",
    1)
    val dfNSData = rddNSData.map(d => SpamDocument(d._1,d._2, 0)).toDF()
    dfNSData.show()
    

    第三步:现在,把数据集汇聚起来,然后根据70%和30%的比例来把整份数据拆分成训练数据和测试数据。

    //
    // Aggregate both data frames
    //
    val dfAllData = dfSData.unionAll(dfNSData)
    dfAllData.show()
    
    //
    // Split the data into 70% training data and 30% test data
    //
    val Array(trainingData, testData) =
    dfAllData.randomSplit(Array(0.7, 0.3))
    

    第四步:现在可以配置机器学习数据流水线了,要创建我们在文章前面部分讨论到的几个部分:Tokenizer、HashingTF和IDF。然后再用训练数据创建回归模型,在这个例子中是逻辑回归。

    //
    // Configure the ML data pipeline
    //
    
    //
    // Create the Tokenizer step
    //
    val tokenizer = new Tokenizer()
      .setInputCol("text")
      .setOutputCol("words")
    
    //
    // Create the TF and IDF steps
    //
    val hashingTF = new HashingTF()
      .setInputCol(tokenizer.getOutputCol)
      .setOutputCol("rawFeatures")
    
    val idf = new
    IDF().setInputCol("rawFeatures").setOutputCol("features")
    
    //
    // Create the Logistic Regression step
    //
    val lr = new LogisticRegression()
      .setMaxIter(5)
    lr.setLabelCol("label")
    lr.setFeaturesCol("features")
    
    //
    // Create the pipeline
    //
    val pipeline = new Pipeline()
      .setStages(Array(tokenizer, hashingTF, idf, lr))
    
    val lrModel = pipeline.fit(trainingData)
    println(lrModel.toString())
    

    第五步:最后,我们调用逻辑回归模型中的转换方法来用测试数据做预测。

    //
    // Make predictions.
    //
    val predictions = lrModel.transform(testData)
    
    //
    // Display prediction results
    //
    predictions.select("file", "text", "label", "features", "prediction").show(300)
    

    结论

    Spark机器学习库是Apache Spark框架中最重要的库之一。它用于实现数据流水线。在这篇文章中,我们了解了如何使用Spark ML包的API以及用它来实现一个文本分类用例。

    接下来的内容

    图数据模型是关于在数据模型中不同的实体之间的连接和关系的。图数据处理技术最近受到了很多关注,因为可以用它来解决许多问题,包括欺诈检测和开发推荐引擎等。

    Spark框架提供了一个库,专门用于图数据分析。我们在这个系列的文章中,接下来会了解这个名为Spark GraphX的库。我们会用Spark GraphX来开发一个示例程序,用于图数据处理和分析。

    引用

  • Scala编码规范

    格式与命名

    1) 代码格式用两个空格缩进。避免每行长度超过100列。在两个方法、类、对象定义之间使用一个空白行。

    2) 优先考虑使用val,而非var。

    3) 当引入多个包时,使用花括号:

    import jxl.write.{WritableCell, Number, Label}

    当引入的包超过6个时,应使用通配符_:

    import org.scalatest.events._

    4)若方法暴露为接口,则返回类型应该显式声明。例如:

    def execute(conn: Connection) : Boolean =
    {
          executeCommand(conn, sqlStatement) match
          {
            case Right(result) => result
            case Left(_) => false
          }
    }

    5) 集合的命名规范

    xs, ys, as, bs等作为某种Sequence对象的名称;

    x, y, z, a, b作为sequence元素的名称。

    h作为head的名称,t作为tail的名称。

    6)避免对简单的表达式采用花括号;

    //suggestion
    def square(x: Int) = x * x
    
    //avoid
    def square(x: Int) =
    {
      x * x
    }

     

    7) 泛型类型参数的命名虽然没有限制,但建议遵循如下规则:

    A 代表一个简单的类型,例如List[A]

    B, C, D 用于第2、第3、第4等类型。例如:

    class List[A] {

    def mapB: List[B] = …

    }

    N 代表数值类型

    注意:在Java中,通常以K、V代表Map的key与value,但是在Scala中,更倾向于使用A、B代表Map的key与value。

    8)数值类型变量

    scala有7种数值类型:Byte、Char、Short、Int、Long、Float和Double,以及2种非数值类型:Boolean和Unit(只有一个值“()”,相当于java和c++中的void,即空值)。

    这些类型都是抽象的final类(不能使用new新建,也不能被继承),在scala包中定义,是对java基本数据类型的包装,因此与java基本数据类型有相同的长度。

    同时,scala还提供了RichInt、RichChar等等,它们分别提供Int、Char等所不具备的便捷方法。

    另外,scala沿用了java.lang包中的String。在scala中,常量也称作字面量,字符串字面量由双引号包含的字符组成,同时scala提供了另一种定义字符串常量的语法——原始字符串,它以三个双引号作为开始和结束,字符串内部可以包含无论何种任意字符。

    在scala中,我们使用方法,而不是强制类型转换,来做数值类型之间的转换,如99.44.toInt、97.toChar。另外也可以参见显式类型转换和隐式转换。

     

    1.1.    命名规则

    1.1.1.      程序文件

    采用unix标准,比如user_info.scala,而不使用win32标准的UserInfo.scala。

    文件后缀说明

    后缀

    说明

    .scala

    scala源代码文件

    .py

    python源代码文件

    .java

    java源代码文件

    .r

    r源代码文件

    .proto protobuf协议文件

    .iml 、.xml

    配置文件

    .htm、.html、.shtml

    页面文件

    .sql

    SQL脚本、hivesql脚本

    .sh

    /bin/sh脚本

    Makefile

    make文件,无后缀

    1.1.2.      变量、函数、类

    个体类型前缀,驼峰式命名方法。

    前缀

    基本数据类型

    说明

    举例

    b

    Boolean

    Boolean bStatus

    ch

    Char

    Char chStx

    l

    Long

    *因为32位操作系统和64操作系统在数据长度上的问题,强烈要求不要使用

    Long lTimeValue

    i

    Int

    Int iFunReturn

    sh

    Short

    多用于表示尺寸

    Short shCodeLen

    s

    java.lang包中的String。

    String sUsrName

    f

    Float

    Float fMoney

    d

    Double

    Double dMoney

    o

    scala类实例对象

    Vector

    List

    Queue

    Array

    HashMap

    HashSet

    Map

    通常命名时4个字母一下
    全名,非时采用缩进剪短命名
    在加变量本身含义
    比如:

    Vector oVecSeq

    List oListVal
    HashMap oHMapVal

     

    生存周期前缀列表

    生命周期范围

    前缀

    备注

    全局变量

    g_

    String g_sProgramName

    类生存周期变量

    m_

    String CClass::m_sName

    语法特性

    1) 定义隐式类时,应该将构造函数的参数声明为val。

    2)使用for表达式;如果需要条件表达式,应将条件表达式写到for comprehension中:

    //not good
    for (file <- files) {
      if (hasSoundFileExtension(file) && !soundFileIsLong(file)) {
        soundFiles += file
      }
    }
    
    //better
    for {
      file <- files
      if hasSoundFileExtension(file)
      if !soundFileIsLong(file)
    } yield file

     

    通常情况下,我们应优先考虑filter, map, flatMap等操作,而非for comprehension:

    //best
    files.filter(hasSourceFileExtension).filterNot(soundFileIsLong)

    3) 避免使用isInstanceOf,而是使用模式匹配,尤其是在处理比较复杂的类型判断时,使用模式匹配的可读性更好。

    //avoid
    if (x.isInstanceOf[Foo]) { do something …
    
    //suggest
    def isPerson(x: Any) :  Boolean = x match {
        case p: Person => true
     case _ => false
    }

     

    4)以下情况使用abstract class,而不是trait:

    • 想要创建一个需要构造函数参数的基类
    • 代码可能会被Java代码调用

    5) 如果希望trait只能被某个类(及其子类)extend,应该使用self type:

    trait MyTrait { this: BaseType => }   

    如果希望对扩展trait的类做更多限制,可以在self type后增加更多对trait的混入:

    trait WarpCore {
     this: Starship with WarpCoreEjector with FireExtinguisher =>
    }
    // this works
    class Enterprise extends Starship 
    with WarpCore 
    with WarpCoreEjector 
    with FireExtinguisher
     
    // won't compile 
    class Enterprise extends Starship  
    with WarpCore  
    with WarpCoreEjector 
     

    如果要限制扩展trait的类必须定义相关的方法,可以在self type中定义方法,这称之为structural type(类似动态语言的鸭子类型):

    trait WarpCore {
       this: {
        def ejectWarpCore(password: String): Boolean
        def startWarpCore: Unit
       } =>
    }
    class Starship class Enterprise extends Starship with WarpCore { 
       def ejectWarpCore(password: String): Boolean = {
        if (password == "password") { println("core ejected"); true } else false }
       def startWarpCore { println("core started") }
    }
    

    6) 对于较长的类型名称,在特定上下文中,以不影响阅读性和表达设计意图为前提,建议使用类型别名,它可以帮助程序变得更简短。例如:

    class ConcurrentPool[K, V] { 
       type Queue = ConcurrentLinkedQueue[V]
       type Map   = ConcurrentHashMap[K, Queue]  
    }

    7) 如果要使用隐式参数,应尽量使用自定义类型作为隐式参数的类型,而避免过于宽泛的类型,如String,Int,Boolean等。

    //suggestion
    def maxOfList[T](elements: List[T])
       (implicit orderer: T => Ordered[T]): T =
     elements match {
      case List() =>
       throw new IllegalArgumentException("empty list!")
      case List(x) => x
      case x :: rest =>
       val maxRest = maxListImpParm(rest)(orderer)
       if (orderer(x) > maxRest) x
       else maxRest
     }
    //avoid
    def maxOfListPoorStyle[T](elements: List[T])
        (implicit orderer: (T, T) => Boolean): T
    

    8) 对于异常的处理,Scala除了提供Java风格的try…catch…finally之外,还提供了allCatch.opt、Try…Success…Failure以及Either…Right…Left等风格的处理方式。其中,Try是2.10提供的语法。根据不同的场景选择不同风格:

    优先选择Try风格。Try很好地支持模式匹配,它兼具Option与Either的特点,因而既提供了集合的语义,又支持模式匹配,又提供了getOrElse()方法。同时,它还可以组合多个Try,并支持运用for combination。

    val z = for {
        a <- Try(x.toInt ) b <- Try(y.toInt ) } yield a * b val answer = z.getOrElse (0) * 2 

    如果希望清楚的表现非此即彼的特性,应考虑使用Either。

    注意,约定成俗下,我们习惯将正确的结果放在Either的右边(Right既表示右边,又表示正确)

    如果希望将异常情况处理为None,则应考虑使用allCatch.opt。

    import scala.util.control.Exception._
    
    def readTextFile(f: String) :  Option[List[String]] =     
        allCatch.opt(Source.fromFile(f).getLines.toList)

    如果希望在执行后释放资源,从而需要使用finally时,考虑try…catch…finally,或者结合try…catch…finally与Either。

    private def executeQuery(conn: Connection, sql: String) :  Either[SQLException, ResultSet] = {
        var stmt: Statement = null
        var rs: ResultSet = null
        try {
          stmt = conn.createStatement()
          rs = stmt.executeQuery(sql)
          Right(rs)
        } catch {
          case e: SQLException => {
            e.printStackTrace()
            Left(e)
          }
        } finally {
          try {
            if (rs != null) rs.close()
            if (stmt != null) stmt.close()
          } catch {
            case e: SQLException => e.printStackTrace()
          }
        }
      }

    为避免重复,还应考虑引入Load Pattern。

    编码风格

    1) 尽可能直接在函数定义的地方使用模式匹配。例如,在下面的写法中,match应该被折叠起来(collapse):

    list map { item =>   
         item match {     
              case Some(x) => x     
              case None => default   
         } 
    }

    用下面的写法替代:

    list map {
       case Some(x) => x
       case None => default 
    }

    它很清晰的表达了 list中的元素都被映射,间接的方式让人不容易明白。此时,传入map的函数实则为partial function。

    2)避免使用null,而应该使用Option的None。

    import java.io._
    object CopyBytes extends App {
     var in = None: Option[FileInputStream]
     var out = None: Option[FileOutputStream]
     try {
      in = Some(new FileInputStream("/tmp/Test.class"))
      out = Some(new FileOutputStream("/tmp/Test.class.copy"))
      var c = 0
      while ({c = in.get.read; c != 1}) {
         out.get.write(c)
        }
     } catch {
      case e: IOException => e.printStackTrace
     } finally {
      println("entered finally ...")
      if (in.isDefined) in.get.close
      if (out.isDefined) out.get.close
     }
    }
    

    方法的返回值也要避免返回Null。应考虑返回Option,Either,或者Try。例如:

    import scala.util.{Try, Success, Failure} 
    def readTextFile(filename: String) :  Try[List[String]] = { 
     Try(io.Source.fromFile(filename).getLines.toList
    )
    val filename = "/etc/passwd" 
    readTextFile(filename) match {
     case Success(lines) => lines.foreach(println)
     case Failure(f) => println(f) 
    }
    

    3)若在Class中需要定义常量,应将其定义为val,并将其放在该类的伴生对象中:

    class Pizza (var crustSize: Int, var crustType: String) { 
     def this(crustSize:  Int)  {
      this(crustSize, Pizza.DEFAULT_CRUST_TYPE)
     }
     def this(crustType:  String)  {
      this(Pizza.DEFAULT_CRUST_SIZE, crustType)
     }
     def this() {
      this(Pizza.DEFAULT_CRUST_SIZE, Pizza.DEFAULT_CRUST_TYPE)
     }
     override def toString = s"A $crustSize inch pizza with a $crustType crust"
    }
    object Pizza {
     val DEFAULT_CRUST_SIZE = 12
     val DEFAULT_CRUST_TYPE = "THIN"
    }
    

    4)合理为构造函数或方法提供默认值。例如:

    class Socket (val timeout: Int = 10000)  

    5)如果需要返回多个值时,应返回tuple。

    def getStockInfo = {
         //
         ("NFLX", 100.00, 101.00)
    }

    6) 作为访问器的方法,如果没有副作用,在声明时建议定义为没有括号。

    例如,Scala集合库提供的scala.collection.immutable.Queue中,dequeue方法没有副作用,声明时就没有括号:

    import scala.collection.immutable.Queue
    
    val q = Queue(1, 2, 3, 4)
    val value = q.dequeue

    7) 将包的公有代码(常量、枚举、类型定义、隐式转换等)放到package object中。

    package com.agiledon.myapp
    package object model {
      // field
     val MAGIC_NUM = 42 182 | Chapter 6: Objects
      
     // method
     def echo(a: Any) { println(a) }
      // enumeration
     object Margin extends Enumeration {
        type Margin = Value
        val TOP, BOTTOM, LEFT, RIGHT = Value
      }
      // type definition
     type MutableMap[K, V] = scala.collection.mutable.Map[K, V]
      val MutableMap = scala.collection.mutable.Map
    }
    
    

    8) 建议将package object放到与包对象命名空间一致的目录下,并命名为package.scala。以model为例,package.scala文件应放在:

    +– com

    +– agiledon

    +– myapp

    +– model

    +– package.scala

    9) 若有多个样例类属于同一类型,应共同继承自一个sealed trait。

    sealed trait Message
    case class GetCustomers extends Message case class GetOrders extends Message 

    注:这里的sealed,表示trait的所有实现都必须声明在定义trait的文件中。

    10) 考虑使用renaming clause来简化代码。例如,替换被频繁使用的长名称方法:

    import System.out.{println => p}
    
    p("hallo scala")
    p("input")

    11) 在遍历Map对象或者Tuple的List时,且需要访问map的key和value值时,优先考虑采用Partial Function,而非使用_1和_2的形式。例如:

    val dollar = Map("China" -> "CNY", "US" -> "DOL")
    
    //perfer
    dollar.foreach {
         case (country, currency) => println(s"$country -> $currency")
    }
    
    //avoid
    dollar.foreach ( x => println(s"$x._1 -> $x._2") )

    或者,考虑使用for comprehension:

    for ((country, currency) <- dollar) println(s"$country -> $currency")

    12) 遍历集合对象时,如果需要获得并操作集合对象的下标,不要使用如下方式:

    val l = List("zero", "one", "two", "three")
    
    for (i <- 0 until l.length) yield (i, l(i))

    而应该使用zipWithIndex方法:

    for ((number, index) <- l.zipWithIndex ) yield (index, number) 

    或者:

    l.zipWithIndex.map(x => (x._2, x._1))

    当然,如果需要将索引值放在Tuple的第二个元素,就更方便了。直接使用zipWithIndex即可。

    zipWithIndex的索引初始值为0,如果想指定索引的初始值,可以使用zip:

    l.zip(Stream from 1)

    13) 应尽量定义小粒度的trait,然后再以混入的方式继承多个trait。例如ScalaTest中的FlatSpec:

    class FlatSpec extends FlatSpecLike ... trait FlatSpecLike extends Suite with ShouldVerb with MustVerb with CanVerb with Informing  

    小粒度的trait既有利于重用,同时还有利于对业务逻辑进行单元测试,尤其是当一部分逻辑需要依赖外部环境时,可以运用“关注点分离”的原则,将不依赖于外部环境的逻辑分离到单独的trait中。

    14) 优先使用不可变集合。如果确定要使用可变集合,应明确的引用可变集合的命名空间。不要用使用import scala.collection.mutable._;然后引用 Set,应该用下面的方式替代:

    import scala.collections.mutable
    val set = mutable.Set() 

    这样更明确在使用一个可变集合。

    15) 在自己定义的方法和构造函数里,应适当的接受最宽泛的集合类型。通常可以归结为一个: Iterable, Seq, Set, 或 Map。如果你的方法需要一个 sequence,使用 Seq[T],而不是List[T]。这样可以分离集合与它的实现,从而达成更好的可扩展性。

    16) 应谨慎使用流水线转换的形式。当流水线转换的逻辑比较复杂时,应充分考虑代码的可读性,准确地表达开发者的意图,而不过分追求函数式编程的流水线转换风格。例如,我们想要从一组投票结果(语言,票数)中统计不同程序语言的票数并按照得票的顺序显示:

    val votes = Seq(("scala", 1), ("java", 4), ("scala", 10), ("scala", 1), ("python", 10))
    val orderedVotes = votes
       .groupBy(_._1)
       .map { case (which, counts) =>
         (which, counts.foldLeft(0)(_ + _._2))
       }.toSeq
       .sortBy(_._2)
       .reverse

    上面的代码简洁并且正确,但几乎每个读者都不好理解作者的原本意图。一个策略是声明中间结果和参数:

    val votesByLang = votes groupBy { case (lang, _) => lang }
    val sumByLang = votesByLang map {
      case (lang, counts) =>
        val countsOnly = counts map { case (_, count) => count }
        (lang, countsOnly.sum)
    }
    val orderedVotes = sumByLang.toSeq
      .sortBy { case (_, count) => count }
      .reverse
    
    

    代码也同样简洁,但更清晰的表达了转换的发生(通过命名中间值),和正在操作的数据的结构(通过命名参数)。

    17) 对于Options对象,如果getOrElse能够表达业务逻辑,就应避免对其使用模式匹配。许多集合的操作都提供了返回Options的方法。例如headOption等。

    val x = list.headOption getOrElse 0

    这要比模式匹配更清楚:

    val x = list match 
         case head::_ => head
         case Nil: => 0

    18) 当需要对两个或两个以上的集合进行操作时,应优先考虑使用for表达式,而非map,flatMap等操作。此时,for comprehension会更简洁易读。例如,获取两个字符的所有排列,相同的字符不能出现两次。使用flatMap的代码为:

    val chars = 'a' to 'z' 
    val perms = chars flatMap { a => 
       chars flatMap { b => 
         if (a != b) Seq("%c%c".format(a, b))
         else Seq() 
       }
     }

    使用for comprehension会更易懂:

    val perms = for {
       a <- chars
       b <- chars
       if a != b
     } yield "%c%c".format(a, b)

    高效编码

    1) 应尽量避免让trait去extend一个class。因为这种做法可能会导致间接的继承多个类,从而产生编译错误。同时,还会导致继承体系的复杂度。

    class StarfleetComponent trait StarfleetWarpCore extends StarfleetComponent class Starship extends StarfleetComponent with StarfleetWarpCore class RomulanStuff // won't compile class Warbird extends RomulanStuff with StarfleetWarpCore 

    2) 选择使用Seq时,若需要索引下标功能,优先考虑选择Vector,若需要Mutable的集合,则选择ArrayBuffer;若要选择Linear集合,优先选择List,若需要Mutable的集合,则选择ListBuffer。

    3) 如果需要快速、通用、不变、带顺序的集合,应优先考虑使用Vector。Vector很好地平衡了快速的随机选择和快速的随机更新(函数式)操作。Vector是Scala集合库中最灵活的高效集合。一个原则是:当你对选择集合类型犹疑不定时,就应选择使用Vector。

    需要注意的是:当我们创建了一个IndexSeq时,Scala实际上会创建Vector对象:

    scala> val x = IndexedSeq(1,2,3) 
    x: IndexedSeq[Int]  = Vector(1, 2, 3)

    4) 如果需要选择通用的可变集合,应优先考虑使用ArrayBuffer。尤其面对一个大的集合,且新元素总是要添加到集合末尾时,就可以选择ArrayBuffer。如果使用的可变集合特性更近似于List这样的线性集合,则考虑使用ListBuffer。

    5) 如果需要将大量数据添加到集合中,建议选择使用List的prepend操作,将这些数据添加到List头部,最后做一次reverse操作。例如:

    var l = List[Int]()
    (1 to max).foreach {
         i => i +: l
    }
    l.reverse

    6) 当一个类的某个字段在获取值时需要耗费资源,并且,该字段的值并非一开始就需要使用。则应将该字段声明为lazy。

    lazy val field = computation()

    7) 在使用Future进行并发处理时,应使用回调的方式,而非阻塞:

    //avoid
    val f = Future {
      //executing long time
    }
    val result = Await.result(f, 5 second)
    //suggesion
    val f = Future {
      //executing long time
    }
    f.onComplete {
      case Success(result) => //handle result
     case Failure(e) => e.printStackTrace
    }
    
    

    8) 若有多个操作需要并行进行同步操作,可以选择使用par集合。例如:

    val urls = List("http://scala-lang.org", "http://agiledon.github.com")
    
    def fromURL(url:  String)  = scala.io.Source.fromURL(url).getLines().mkString("\n")
    
    val t = System.currentTimeMillis()
    urls.par.map(fromURL(_))
    println("time: " + (System.currentTimeMillis - t) + "ms")

    9) 若有多个操作需要并行进行异步操作,则采用for comprehension对future进行join方式的执行。例如,假设Cloud.runAlgorithm()方法返回一个Futrue[Int],可以同时执行多个runAlgorithm方法:

    val result1 = Cloud.runAlgorithm(10)
    val result2 = Cloud.runAlgorithm(20)
    val result3 = Cloud.runAlgorithm(30)
    
    val result = for {
      r1 <- result1 r2 <- result2 r3 <- result3 } yield (r1 + r2 + r3) result onSuccess { case result =>  println(s"total = $result")
    }

    编码模式

    1) Loan Pattern: 确保打开的资源(如文件、数据库连接)能够在操作完毕后被安全的释放。

    Loan Pattern的通用格式如下:

    def using[A](r : Resource)(f : Resource => A) : A =
       try {
            f(r)
       } finally {
            r.dispose()
       }

    这个格式针对Resource类型进行操作。还有一种做法是:只要实现了close方法,都可以运用Loan Pattern:

    def using[A <:  def close():Unit, B][resource: A](f: A => B): B = 
         try {
              f(resource)
         } finally {
              resource.close()
         }

    以FileSource为例:

    using(io.Source.fromFile("example.txt")) { 
        source => {
            for (line <- source.getLines ) { println(line) } } } 

    2) Cake Pattern: 利用self type实现依赖注入

    例如,对于DbAccessor而言,需要提供不同的DbConnectionFactory来创建连接,从而访问不同的Data Source。

    trait DbConnectionFactory {
         def createDbConnection:  Connection
    }
    
    trait SybaseDbConnectionFactory extends DbConnectionFactory
    trait MySQLDbConnectionFactory extends DbConnectionFactory

    运用Cake Pattern,DbAccessor的定义应该为:

    trait DbAccessor {
         this: DbConnectionFactory => 
    
         //…
    }

    由于DbAccessor使用了self type,因此可以在DbAccessor中调用DbConnectionFactory的方法createDbConnection()。客户端在创建DbAccessor时,可以根据需要选择混入的DbConnectionFactory:

    val sybaseDbAccessor = new DbAccessor with SybaseDbConnectionFactory

    当然,也可以定义object:

    object SybaseDbAccessor extends DbAccessor with SybaseDbConnectionFactory
    object MySQLDbAccessor extends DbAccessor with MySQLDbConnectionFactory

    测试

    1) 测试类应该与被测试类处于同一包下。如果使用Spec2或ScalaTest的FlatSpec等,则测试类的命名应该为:被测类名 + Spec;若使用JUnit等框架,则测试类的命名为:被测试类名 + Test

    2) 测试含有具体实现的trait时,可以让被测试类直接继承Trait。例如:

    trait RecordsGenerator {
     def generateRecords(table: List[List[String]]): List[Record] {
      //...
     }
    }
    class RecordsGeneratorSpec extends FlatSpec with ShouldMatcher with RecordGenerator { 
     val table = List(List("abc", "def"), List("aaa", "bbb"))
     it should "generate records" in {
      val records = generateRecords(table)
      records.size should be(2)
     }
    }
    

    3) 若要对文件进行测试,可以用字符串假装文件:

    type CsvLine = String
    def formatCsv(source: Source) :  List[CsvLine] = {
         source.getLines(_.replace(", ", "|"))
    }

    formatCsv需要接受一个文件源,例如Source.fromFile(“testdata.txt”)。但在测试时,可以通过Source.fromString方法来生成formatCsv需要接收的Source对象:

    it should "format csv lines" in {
         val lines = Source.fromString("abc, def, hgi\n1, 2, 3\none, two, three")
         val result = formatCsv(lines)
         result.mkString("\n") should be("abc|def|hgi\n1|2|3\none|two|three")
    }

     

    避免直接借用其他语言的编码规范

     

    本文将讨论Scala中的一些编码规范,它们有助于减少编译和运行时的错误。

    大家都有几门开发语言的编写能力,每种语言又有自己的语法格式和高质量编程要求,也有一些编码规范是在各种语言通用的,比如良好的注释。

    但Scala与我们经常碰到的Java,C,C++还是有很大的不同,在深入理解Scala之前,最好谨慎使用其他语言相关的编码规范。

    编码规范在团队开发中必须的。它帮助团队避免一些曾经出现的错误,提供代码层面交流的一致性语言。我们也许没有去看自己公司、团队的编码规范,但可以从代码中略知一二。

    建立团队的编码规范的步骤有:

    • 首先建立预防错误的一些规则。这些规则可能是来自使用相同语言的其他项目。然后自己添加一些从过去出错项目中总结的一些规则。比如C++中析构函数应该声明为虚函数。
    • 然后根据团队自己的开发环境,来发现、定义一些新的编码规范。比如包的命名方式。
    • 坚持执行前面的制定的规则。最好有一个自动化工具,能够检测我们的编码是否符合规范,然后可以自动做一些重构工作。

    核心规则8条

    大家肯定见过关于左大括号应该换行写,还是同行写的争论。当然这个争论无关痛痒,对编译器来说没有什么影响。我们看一个Scala例子:

    class FooHolder
    {
      def foo1()
      {
        println("foo1 was called")
      }
      def foo2()Unit =
      {
        println("foo2 was called")
      }
      def foo3() =
        println("foo3 was called")
    }

     

    foo1、foo2、foo3都是正确的。都是定义一个函数,然后输出一个字符串。不同的是书写风格。foo1类似于C语言风格,但是没有指定返回值;foo2是标准完整的Scala函数定义,有返回值,有表达式;foo3虽然没有返回值,但是有表达式赋值。

    需要注意的是,对于foo3,如果没有=。那么在实例化一个FooHolder的时候编译器会报错:class FooHolder needs to be abstract, since method foo3 is not defined。因为编译器会把它认为是一个抽象函数,这样的类去实例化是不允许的。虽然Scala给大家提供了一个非常宽松的环境,但为了避免类似的错误,也从人们理解上考虑,避免歧义,避免猜测,建议大家使用第二种foo2的方式。

    悬垂操作符

    dangling operator怎么翻译,没有找到一个现成的答案,那就自己定义一个名字:悬垂操作符。它指的是位于每行最后的操作符,比如+、-都可以作为悬垂操作符,它告诉Scala编译器本行还没有结束。

    在Java中我们连接一个字符串可以随便写,同行也可以,分行也可以。但是Scala中,操作符需要考虑它们所在的位置。比如下面的代码:

    val x = 5   def foo2 = "HAI"     + x
        + "ZOMG"
        + "\n"

     

    这个函数编译是会出错的:value unary_+ is not a member of String。String没有一元操作符+(Scala直接使用的Java的String)。但是x(类型为Int,Scala自己的一个类型)却是有的,所以+x没有报错。

    为了解决这个编译错误,我们有两种办法:

    一是告诉编译器+表示的是一行尚未完,即使用悬垂操作符:

      val x = 5   def foo1 = "HAI" +     x +
        "ZOMG" +
        "\n"

     

    二是加上括号:

      def foo2 = ("HAI"     + x     + "ZOMG"     + "\n")

     

    使用有意义的变量名

    一般的语言标识符只能是字母、数字和下划线,外加一些限制。相比之下,Scala提供非常灵活的命名方式。Scala有三种方法可以构造一个标识符:

    第一,首字符是字母,后续字符是任意字母和数字。这种标识符还可后接下划线‟_‟,然后是任意字母和数字。

    第二,首字符是算符字符,后续字符是任意算符字符。这两种形式是普通标识符。

    最后,标识符可以是由反引号‟`‟括起来的任意字符串(宿主系统可能会对字符串和合法性有些限制)。这种标识符可以由除了反引号的任意字符构成。

    第二条规则的存在,很容易让人回想起C++的操作符重载。Scala直接将其当作标识符来处理,应该是更进来一步。

    避免在标示符中使用$。因为编译器内部为内部类、闭包等生成的内部标示符使用了$。如果出现同名,将会导致代码奇怪的行为。有兴趣的话,可以看看编译生成的汇编代码。

    在Scala2.8引入命名参数和默认参数。命名参数会作为API的一部分,名字的改变会使客户端代码出错。因此请使用有意义的名字来对参数进行命名。这就是核心规则6。

    class Foo {
      def foo(one: Int = 1,
              two: String = "two",
              three: Double = 2.5): String =
        two + one + three
    }
    object Test extends scala.App{
      val x = new Foo
      println(x.foo())
      println(x.foo(two = "not two"))
      println(x.foo(0,"zero",0.1))
      println(x.foo(4three = 0.4))
      println(x.foo(three = 0.4one = 3two = "two here"))
    }

     

    运行结果:

    two12.5

    not two12.5

    zero00.1

    two40.4

    two here30.4

    C++也有默认参数。但是没有参数命名。这样C++就有一些限制,需要默认参数放到右边。Scala参数有了名字,调用的时候,它们的顺序就可以任意存放。但如果调用的时候,有的参数直接传值,有的使用参数名字,比如上面的x.foo(4, three = 0.4),我们就需要注意没有使用名字的参数的顺序。

    Scala使用变量的静态类型来绑定参数名字,但是缺省值是由运行时类型决定的。一句话:名字是静态的、值是动态。看一个例子:

    class Parent {
      def foo(bar: Int = 1, baz: Int = 2): Int =
        bar + baz
    }
    class Child extends Parent {
      override def foo(baz: Int = 3, bar: Int = 4): Int =
        super.foo(baz,bar)
    }
    object Test extends scala.App{
      val p = new Parent
      println(p.foo())
      val y = new Child
      println(y.foo())
      val z: Parent = new Child
      println(z.foo())
      println(y.foo(bar = 1))
      println(z.foo(bar = 1))
      println(z.foo(baz = 4))
    }

     

    输出如下:

    3

    7

    7

    4

    5

    7

    Parent定义了foo,子类Child覆盖了foo。z.foo()使用的是缺省值,缺省值是由z的运行时类型Child提供的,所以baz=3,bar=4,输出为7。y.foo(bar = 1)运行时类型和静态类型都是Child,所以baz=3,bar=1,输出为4。z.foo(bar = 1)的运行时类型是Child,静态类型是Parent,函数使用静态类型的,即Parent的def foo(bar: Int = 1, baz: Int = 2): Int。但要注意的是Child和Parent的命名参数位置是反的。缺省值使用静态类型的,所以baz=4(Parent的baz对应Child的bar),bar=1,输出为5。同样的,可以得到z.foo(baz = 4)的结果是7。

    可以看到对于交换了命名参数位置的重载,从理解上看,不直观,比较困难。所有大家应该保持重载的命名参数是一致的。不要随意交换它们的位置。

    重载带有命名参数的函数,可以修改函数的默认值。

    告诉大家这是重载函数

    核心规则7:Scala中,虽然有的时候override是可选的,但是坚持使用override是安全的。

    Trait与Java的interface类似,但是可以拥有方法体,并且可以在类实例化的时候混入。我们编写下面的代码:

    trait UserService {
      def login(credentials: Credentials): UserSession
      def logout(session: UserSession): Unit
      def isLoggedIn(session: UserSession): Boolean
      def changePassword(new_credentials: Credentials,
                         old_credentials: Credentials): Boolean
    }
    class UserServiceImpl extends UserService {
      def login(credentials: Credentials): UserSession =
        new UserSession {}
      def logout(session: UserSession): Unit //class UserServiceImpl needs to be abstract, since method logout is not defined
      def isLoggedIn(session: UserSession): Boolean = true
      def changePassword(session: UserSession,
                         credentials: Credentials): Boolean = true
    }

     

    编译失败。因为编译器发现UserServiceImpl的logout还是一个抽象函数,没有实现,这在class中是不允许的。还有一个错误是UserService的ChangePassword作为一个抽象函数也没有实现。

    如果我们给UserServiceImp加上override关键字,也可以通过编译:

    trait UserServiceImpl extends UserService {
      override def login(credentials: Credentials): UserSession =
        new UserSession {}
      override def logout(session: UserSession): Unit
      override def isLoggedIn(session: UserSession): Boolean = true
      override def changePassword(session: UserSession,                      credentials: Credentials): Boolean = true
    }

     

    这是因为Scala在实现一个抽象方法的时候,不需要override关键字。这样的设置是为了解决多重继承的菱形继承问题。举个例子:

    trait Animal {
      def talk: String
    }
    trait Cat extends Animal {
      override def talk: String = "Meow"
    }
    trait Dog extends Animal {
      override def talk: String = "Woof"
    }
    object Test extends scala.App
    {
      val kittydoggy = new Cat with Dog
      println(kittydoggy.talk) //Woof
      val kittydoggy2 = new Dog with Cat
      println(kittydoggy2.talk) //Meow
    }

     

    大家可能不理解为什么输出是这样?这是Scala的类线性化(Class linearization)决定的。当存在多个函数的多个重载实现的时候,Scala从类声明的最右边开始,在每个类中寻找函数,如果找到,就停止查找。new Cat with Dog会先看Dog有没有定义talk,Dog定义了talk,并返回Woof,所以输出是Woof。类似地,new Dog with Cat会在Cat里面找talk函数。

    如果我们去掉Dog和Cat里面的override。编译器会报下面的错误:

    anonymous class $anon inherits conflicting members:

    method talk in trait Cat of type => String and

    method talk in trait Dog of type => String

    (Note: this can be resolved by declaring an override in anonymous class $anon.)

    val kittydoggy = new Cat with Dog

    ^

    anonymous class $anon inherits conflicting members:

    method talk in trait Dog of type => String and

    method talk in trait Cat of type => String

    (Note: this can be resolved by declaring an override in anonymous class $anon.)

    val kittydoggy2 = new Dog with Cat

    ^

    如果我们只是去掉Dog的override,编译器会报:

    anonymous class $anon inherits conflicting members:

    method talk in trait Cat of type => String and

    method talk in trait Dog of type => String

    (Note: this can be resolved by declaring an override in anonymous class $anon.)

    val kittydoggy = new Cat with Dog

    ^

    在Cat中混入Dog是不允许的,因为Dog没有说它的talk方法可以被重载。反过来,Dog中混入Cat是可以的,因为Cat说明了自己的talk方法可以被重载。

    为了重载某个方法,我们可以在类实例化的时候混入trait,而不需要定义一个新的类。

    对期待的优化使用注解

    Scala编译器在生成字节码的时候做一些优化操作:

    • 优化尾递归【注1】
    • 优化模式匹配

    Scala通过将模式匹配当做switch来处理,来优化模式匹配的效率。模式匹配优化时,编译器编译生成分支表,而不是决策树。这意味着,我们不是在拿值做比较。而是用匹配的值来直接定位分支表。通过JVM的tableswitch操作码可以直接完成。

    Scala使用tableswitch进行优化,必须满足3个条件:

    1. 用俩匹配的值必须是一个已知的整数
    2. 每个匹配表达式必须足够简单:不能包含类型检查、if语句和extractors。如果是表达式需要在编译时就是可用的:它保持不变,不能在运行时求值。
    3. 至少有两个以上分支,否则优化是没有必要的。

    我们继续看个例子:

      def unannotated(x: Int) = x match {
        case 1 => "One"
        case 2 => "Two!"
        case z => z + "?"
      }

     

    使用javap -c得到汇编代码:

    public java.lang.String unannotated(int);

    Code:

    0: iload_1

    1: istore_2

    2: iload_2

    3: tableswitch{ //1 to 2

    1: 51;

    2: 46;

    default: 24 }

    24: new #12; //class scala/collection/mutable/StringBuilder

    27: dup

    28: invokespecial #16; //Method scala/collection/mutable/StringBuilder.”<init>”:()V

    31: iload_2

    32: invokevirtual #20; //Method scala/collection/mutable/StringBuilder.append:(I)Lscala/collection/mutable/StringBuilder;

    35: ldc #22; //String ?

    37: invokevirtual #25; //Method scala/collection/mutable/StringBuilder.append:(Ljava/lang/Object;)Lscala/collection/mutable/StringBuilder;

    40: invokevirtual #29; //Method scala/collection/mutable/StringBuilder.toString:()Ljava/lang/String;

    43: goto 53

    46: ldc #31; //String Two!

    48: goto 53

    51: ldc #33; //String One

    53: areturn

    在得到参数后,放到临时变量,然后调用tableswith指令,通过索引访问跳转表,并跳转。

    我们修改一下上面的代码:

    def notOptimised(x: Int) = x match {
    case 1 => "One"
    case 2 => "Two!"
    case i: Int => "Other"
    }

     

    在最后一个分支上,加上了类型检查。这样就不满足前面的条件2,Scala不会进行优化了。

    public java.lang.String notOptimised(int);

    Code:

    0: iload_1

    1: istore_2

    2: iconst_1

    3: iload_2

    4: if_icmpne 13

    7: ldc #12; //String One

    9: astore_3

    10: goto 27

    13: iconst_2

    14: iload_2

    15: if_icmpne 24

    18: ldc #14; //String Two!

    20: astore_3

    21: goto 27

    24: ldc #16; //String Other

    26: astore_3

    27: aload_3

    28: areturn

    使用if_icmpne指令来判断两个int是否相等,进而决定是不是需要进行跳转。

    我们如何知道编译器是否做了模式匹配的优化呢?Scala可以给类型表达式使用注解。@switch告诉编译器我想做tableswitch优化。如果编译器发现它做不了优化就会报错。比如下面的代码:

    import annotation.switch
    class Tableswitch{
      def annotated(x: Int @switch) = x match {
        case 1 => "One"
        case 2 => "Two!"
        case z => z + "?"
      }
      def notOptimised(x: Int) =
        (x: @switch) match {
          case 1 => "One"
          case 2 => "Two!"
          case i: Int => "Other"
        }
    }

     

    在第11行会报错:

    could not emit switch for @switch annotated match

    (x: @switch) match {

    ^

    @tailrec注解用来告诉编译器,请对尾递归进行优化。

    核心规则8:对需要优化的尾递归,加上@tailrec注解,保证我们得到期望的性能优化。

    Scala编译器进行尾递归优化也需要满足3个条件:

    1. 方法必须声明为final或private,不能是多态的。
    2. 方法必须有返回值注解。
    3. 方法必须在其一个返回分支上最后调用自己。

    我们有一个普通递归和一个尾递归:

    import annotation.tailrec
    object TailRecursion{
      def FibonacciRecursive(n: Int): Int = {
        if(n < 2)
          n
        else
          FibonacciRecursive(n-1)+FibonacciRecursive(n-2)
      }
      @tailrec
      def FibonacciTailRecursive(n: Int, ret1: Int, ret2: Int): Int = {
        if(n < 2)
          ret1
        else
          FibonacciTailRecursive(n-1, ret2, ret1 + ret2)
      }
    }

     

    它们的汇编代码:

    public int FibonacciRecursive(int);

    Code:

    0: iload_1

    1: iconst_2

    2: if_icmpge 9

    5: iload_1

    6: goto 24

    9: aload_0

    10: iload_1

    11: iconst_1

    12: isub

    13: invokevirtual #16; //Method FibonacciRecursive:(I)I

    16: aload_0

    17: iload_1

    18: iconst_2

    19: isub

    20: invokevirtual #16; //Method FibonacciRecursive:(I)I

    23: iadd

    24: ireturn

    public int FibonacciTailRecursive(int, int, int);

    Code:

    0: iload_1

    1: iconst_2

    2: if_icmpge 7

    5: iload_2

    6: ireturn

    7: iload_1

    8: iconst_1

    9: isub

    10: iload_3

    11: iload_2

    12: iload_3

    13: iadd

    14: istore_3

    15: istore_2

    16: istore_1

    17: goto 0

    可见Scala对后者进行了优化。使用的是goto循环。但是如果我们在FibonacciRecursive加上@tailrec注解,编译器就会报错:

    could not optimize @tailrec annotated method FibonacciRecursive: it contains a recursive call not in tail position

    FibonacciRecursive(n-1)+FibonacciRecursive(n-2)

    ^

    优化注解并不是要求编译器去做优化,而是要求编译或发出警告。

    【注1】尾递归指的是函数最后一句调用自身的函数。尾递归优化可以减少递归需要的栈空间。一般将其展开为循环,或者重复使用当前栈空间。尾递归会增加理解的难度,并且不少编译器是不提供尾递归优化的。

     

    参考资料

    来源于网络

    1. Scala Style Guide
    2. Programming in Scala , Martin Odersky
    3. Scala Cookbook , Alvin Alexander
    4. Effective Scala , Twitter
    5. 深入理解Scala-编码规范

  • Spark性能优化指南

    1.优化Spark

    由于大多数Spark计算的内存本质,Spark程序可能因为集群中的任何资源造成瓶颈:CPU,网络,带宽,或者内存。大多数情况下,如果数据可以容纳在内存中,性能瓶颈就是网络带宽,但是有时,你还是需要做一些调优,比如用序列化形式存储RDDs来减少内存使用。这篇指南会覆盖两个主题:数据序列化,这对良好的网络性能是非常关键的,而且也可以减少内存使用;内存调优。我们也讨论了一些小的主题。

    2.数据序列化

    序列化在分布式应用的性能中扮演了一个非常重要的角色。那些序列化对象速度很慢的格式,或者消耗大量字节的格式,会极大的降低性能。通常来说,这应该是你调优一个spark应用时要做的第一件事。spark旨在便捷性(允许你在操作中使用任何java类型)和性能之间取得一个平衡。它提供了两种序列化类库:

     

    · Java序列化:默认情况下,spark使用java的ObjectOutputStream框架来序列化对象,并且对你创建的任何实现了java.io.Serialiable接口的类都有效。你可以控制你的序列化机制的性能,只要实现java.io.Externalizable即可。java序列化机制是灵活的,但是通常是很慢的,对很多类来说,它会导致非常大的序列化格式。

     

    · Kryo序列化:Spark也可以使用Kryo类库来更快地序列化对象。Kryo极大地加快了序列化速度,并且比java序列化格式更加紧凑(通常可以达到10倍)。但是不支持所有的序列化类性,而且要求你自己预先注册在程序中使用的自定义类,来获得最佳的性能。

     

    你可以切换为使用Kryo,只要在使用SparkConf初始化你的应用时,调用conf.set(“spark.serializer”, “org.apache.spark.serializer.KryoSerializer”)。这个配置设置了序列化器,不仅仅是对于在worker node之间shuffle数据,而且也对于将RDDs序列化到磁盘上。Kryo不是默认序列化器的唯一原因是因为它的自定义类注册要求,但是我们推荐在任何网络资源紧张的应用中使用Kryo。

     

    Spark对许多常用的核心Scala类都自动包含了Kryo序列化器。

     

    要注册你自己的自定义类到Kryo上,使用registerKryoClass方法。

     

    valconf=newSparkConf().setMaster(...).setAppName(...)
    conf.registerKryoClasses(Array(classOf[MyClass1],classOf[MyClass2]))
    valsc=newSparkContext(conf)

     

    Kryo文档描述了更多高级的注册选项,比如增加自定义序列化代码。

    如果你的对象很大,你也许需要增加spark.kryoserializer.buffer.mb属性的值。默认是2MB,但是这个值需要足够大到保存你最大的对象。

     

    最后,如果你不注册你的自定义类,Kryo还是会工作,但是它就必须存储每个对象的全限定类名,很浪费内存。

    3.内存调优

    在优化内存使用的时候有三个考虑因素:你的对象使用的内存总量(你可能希望你完整的数据集都保存在内存中),访问这些对象的成本,垃圾回收的开销(如果你有大量对象的处理)。

     

    默认情况下,访问Java对象是非常快的,但是会比它们的field中的原始数据多消耗2-5倍的空间。这是因为多种原因:

     

    · 每个java对象都有一个对象头,大约是16个字节,包含了诸如指向它的类的引用这类信息。对于那些只有很少的数据的对象(比如只有一个int field),对象头会比对象本身的数据要大很多。

     

    · java字符串比原始字符串数据要多出40个字节的开销(因为它将数据存储在一个char数组中,并且还保存了额外的信息,比如字符串的长度),而且因为字符串内部使用了UTF-16编码,所以会使用2个字节存储每个字符。因此一个包含10个字符的字符串可以轻易消耗掉40个字节。

     

    · 普通的集合类,比如HashMap和LinkedList,使用了链式数据结构,对每一个entry对象都有一个包装对象(比如Map.Entry)。这个对象不仅包含对象头,而且还包含了指向列表中下一个对象的引用(通常来说是8个字节)。

     

    · 原始类型的集合通常将原始数据类型使用它们的装箱类型(比如Integer)进行存储。

     

    这个部分会讨论如何确定你的对象的内存使用,以及如何提升它——包含修改你的数据结构,或者使用序列化格式存储数据。我们接着会涵盖Spark内存调优和java垃圾回收的内容。

    3.1 确定内存消耗

    确定你的数据集的内存消耗总量的最佳办法就是,创建一个RDD,将其缓存在内存中,然后查看你的驱动程序的SparkContext日志。日志会告诉你每个分区消耗了多少内存,然后你可以聚合起来计算出RDD的总大小。你可以看见如下的日志信息:

     

    INFO BlockManagerMasterActor: Added rdd_0_1 in memory on mbk.local:50311 (size: 717.5 KB, free: 332.3 MB)

     

    该行日志意味着,RDD 0的分区1消耗了717.5 KB。

    3.2 优化数据结构

    减少内存消耗的第一种方法就是避免会增加内存消耗的java特性,比如基于指针的数据结构(对象)和包装对象。有几种方法来实现它:

     

    1. 在你的数据结构中,优先使用对象和原始数据类型的数组,而不是标准的java集合类(比如HashMap)。fastutil类库为原始数据类型提供了方便的集合类,与java标准类库完全兼容。

    2. 尽可能避免包含大量小对象以及对应的指针的嵌套对象。

    3. 对于key,考虑使用数字类型的ID或者枚举对象,来替代字符串。

    4. 如果你只有小于32G的内存,设置JVM参数-XX:+useCompressedOops,来让指针使用4个字节来替代默认的8个字节。可以在spark-env.sh中增加这些选项。

    3.3 序列化的RDD存储

    如果尽管使用了上述优化技巧,但是你的对象太大了,无法有效地进行存储,一个更简单得减少内存使用的方法就是使用序列化的方式存储它们,在RDD持久化API中使用序列化的存储级别,比如MEMORY_ONLY_SER。Spark会将每个RDD的分区作为一个超大的字节数组进行存储。使用序列化格式进行存储的唯一缺点就是,更慢的访问时间,因为在访问时不得不反序列化每个对象。如果你想使用序列化格式缓存数据,我们强力建议使用Kryo,因为它比java序列化对象可以大量减少空间占用。

    3.4 垃圾回收调优

    JVM垃圾回收也许会成为一个大问题,如果你的程序要存储非常大的RDDs的话。(但是如果只是读取一次RDD,然后对它做很多操作,则这不会成为一个问题)java需要移除旧的对象来为新的对象腾出空间,因此需要追踪所有的java对象,并找出其中不在使用的那些。这里需要记住的一点就是,垃圾回收的消耗是与java对象的数量成正相关的,所以使用包含更少对象的数据结构(比如用int数组替代Integer LinkedList),可以极大地极少这方面的开销。一个更好的方法就是使用序列化格式存储对象,如上所述:现在一个RDD分区仅有一个对象(byte数组)。在尝试其他技巧之前,如果GC是一个问题,则需要尝试的第一个技巧就是使用序列化缓存技术。

     

    GC也可能因为你的任务的工作内存和节点上缓存的RDDs之间的冲突而变为一个问题。我们会讨论如何控制分配给RDD缓存的空间来解决这个问题。

    3.4.1 测量GC的影响

    GC调优的第一个步骤就是收集统计信息——垃圾回收发生的频率以及占用的总时间。这可以通过增加-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps来完成。下次你的spark job运行的时候,你就会看见每次发生垃圾回收时打印在worker日志里的消息。注意这些日志是在你的worker节点上,不在驱动程序中。

    3.4.2 优化缓存大小

    对GC来说,一个重要的配置参数就是用于缓存RDDs的内存量。默认情况下,Spark使用每个executor的内存的60%来缓存RDDs。这意味着只有40%的内存用来容纳任务执行期间创建的对象。

     

    在你的任务执行变慢的情况下,并且你发现你的JVM正在频繁的进行垃圾回收,或者几乎用光所有内存,把这个值降低可以帮助减小内存消耗。如果要改变这个值,比如50%,可以在你的SparkConf上调用conf.set(“spark.storage.memoryFraction”, “0.5”)。结合序列化缓存的使用,使用一个更小的缓存就可以缓和大多数的垃圾回收问题。

    3.4.3 高级GC调优

    为了进一步进行垃圾回收调优,我们首先需要理解JVM中内存管理的一些基本信息:

     

    · Java堆空间被划分成了两个区域,新生代和老年代。新生代用于存储短暂活跃的对象,老年代用于存储长时间存活的对象。

     

    · 新生代进一步划分为了三个区域,Eden,Survivor1,Survivor2.

     

    · 一个简化的垃圾回收过程的描述如下:当Eden区域满了之后,minor GC会在Eden区域和一个Survivor1区域进行,在Eden区域和Survivor1区域中还存活的对象会被拷贝到Survivor2区域中。Survisor区域被交换。如果一个对象年龄足够大,或者拷贝时发现Survivor2区域也满了,则会将该对象移动到老年代。最后,如果老年代满了,就会发生Full GC。

     

    在Spark中GC调优的目的是确保,只有长期存活的对象进入了老年代中,并且新生代是足够大来容纳所有短暂活跃对象的。这可以避免去通过full GC回收任务执行期间创建的临时对象。一些可能有用的操作如下:

     

    · 通过收集GC统计信息来检查是否发生了太多的垃圾回收。如果在一个task完成之前,full GC执行了多次,那么就意味着没有足够的内存来执行任务。

     

    · 在打印出来的GC统计信息中,如果老年代已经接近于满了,减少用于缓存的空间。这可以通过spark.storage.memroyFraction属性来配置。缓存更少的对象比降低任务执行速度更好。

     

    · 如果有很多minor GC,但是没有很多major GC,分配更多的空间给Eden区域。你可以将Eden区域的大小设置为比预估的每个任务要占用的内存更大。如果Eden区域的大小是E,那么你可以使用选项-Xmn=4/3*E来设置年轻代的大小。

     

    · 举例来说,如果你的任务正在从HDFS中读取数据,任务使用的内存总量可以通过使用从HDFS读取出来的数据块的大小来估算。注意,一个解压缩的块的大小通常是块的2-3倍。所以如果我们希望有3个或4个任务的工作空间,并且hdfs的块大小是64MB,我们可以估算Eden区域的大小为4 * 3 * 64 MB。

     

    · 监控随着新的设置,垃圾回收的发生频率以及执行时间。

     

    我们的经验表明,GC调优的影响依赖于你的应用程序以及可用的内存量。有许多调优方法,但是站在一个高的角度来看,管理GC发生的频率可以减少开销。

    4.其他考虑

    4.1 并行级别

    除非你为每个操作设置的并行级别足够高,否则集群是不会被充分使用的。对于每个文件来说,Spark会根据它的大小自动设置map任务的数量。对于分布式的reduce操作,比如groupByKey和reduceByKey,它使用了最大的父RDD的分去数量。你可以传递并行级别作为第二个参数,或者设置spark.default.parallelism。通常我们建议为每个CPU core设置2-3个任务。

    4.2 reduce任务的内存使用

    有时,你遇到一个OutOfMemory异常,不是因为你的RDDs无法在内存中容纳,你的任务的working set,比如groupByKey的reduce任务,太大了。Spark的shuffle操作(sortByKey,groupByKey,reduceByKey,等等)都会在每个任务中构建一个hash table来执行分组,通常hash table是非常大的。最简单的方式就是提高任务的并行度,来让每个任务的输入变小。Spark可以支持200ms的任务,因为它会对多个任务重用一个executor的JVM,因此由非常小的任务启动开销,所以你可以放心提高并行度,哪怕超过了你的应用的CPU core数量。

    4.3 广播大变量

    使用SparkContext提供的广播功能可以大幅度减少每个序列化任务的大小,以及在一个集群上启动一个作业的开销。如果你的任务在内部使用了驱动程序中的大对象,考虑将它变为一个广播变量。Spark会在master上打印每个序列化任务的大小,你可以看它来判断你的任务是不是太大了;通常来说,任务如果超过了20 KB都是值得优化的。

    4.4 数据本地化

    数据本地化会对Spark作业的性能有极大的影响。如果数据和代码都在一起,那么计算是非常快的。但是如果数据和代码是分离的,那么其中之一必须移动到另外一个所在的节点上去。通常来说,将序列化的代码从一个地方移动到另一个地方是比移动一段数据更快的,因为代码的大小比数据的大小更小。Spark针对数据本地化的原则构建了它的调度。

     

    数据本地化是指,数据距离处理它的代码有多近。基于数据当前的位置,有很多种数据本地化级别:

     

    · PROCESS_LOCAL 数据和运行的代码在同一个JVM中。这是最好的本地化。

    · NODE_LOCAL 数据与代码在同一个节点上。例如,数据在同一个节点上的HDFS中,或者在同一个节点的其他executor中。这比PROCESS_LOCAL会稍慢一点,因为需要在进程间移动数据。

    · NO_PREF 数据从哪里获取都是一样快的,没有数据本地化的偏好。

    · RACK_LOCAL 数据与代码在一个机架上。数据在同一个机架的不同服务器上,因此需要通过网络来发送。

    · ANY 数据和代码在网络中的任何地方,并且不在一个机架上。

     

    Spark希望使用最佳的本地化级别来调度所有的任务,但是这是不可能的。在没有未处理的数据在任何空闲的executor中的情况下,Spark会切换到更慢的本地化级别上。有两个选项:a) 等待,直到一个繁忙的CPU空闲下来,可以在有数据的服务器上启动一个任务;b) 立即在任何一个节点上启动任务,然后需要对数据进行移动。

     

    Spark最常见的是等待一下,期望一个CPU空闲下来。但是如果超时以后,它就会开始移动数据到任意一个空闲的CPU上。

  • JAVA知识总结

    1、jdk自带线程池为什么先入队列,后创建非核心线程?
    因为队列增加一个元素的成本更低。

    2、内部类引用外部类方法的局部变量,为什么必须使用final修饰?
    因为外部类方法的变量运行完毕就会回收,但加载的内部类可能仍然存在。

    3、方法的签名包括方法名和参数列表,为什么不包括返回值?
    因为调用端可以不接收返回值,此种情况就无法判断具体是哪个方法。

    4、Spring的线程池工具ThreadPoolTaskExecutor,也是基于JDK的ThreadPoolExecutor实现,它有什么好处、陷阱?
    好处:参数、单例可配置,队列、拒绝策略有默认。缺点:错过了解底层的机会,直接使用ThreadPoolExecutor也不难。

    5、有哪些令人眼花缭乱的锁?
    自旋、阻塞、重入、读写、乐观、悲观、偏向、轻量级、重量级,共享锁、排他锁、行锁、表锁、间隙锁、意向锁。

    6、状态模式与策略模式有什么区别?
    结构上看,没有区别。
    目的上看,
    状态模式由一组状态驱动,用于处理状态的变化;
    策略模式由一组算法驱动,用于处理算法的变化。
    具体来说,
    策略模式中,算法是否变化完全由客户端决定,而且一般一次只能选择一种算法,不存在中途变化的情况;
    状态模式中,状态本身存在线性的生命周期,是否变化由具体状态内部决定,这种变化对客户端是透明的。

    7、HashMap.put(key, value)的执行过程。
    计算key的哈希值。
    计算哈希值在数组中的位置。
    若该key已存在,则替换掉旧值。
    否则,在该位置添加新的节点。
    最后检查是否需要扩容。

    8、HashMap何时扩容?怎么扩容?数据迁移?
    何时扩容?
    put的最有一步,若size>=threshold(capcity*loadFactor),则进行扩容。
    怎么扩容?
    创建一个新数组,容量=旧数组容量*2。
    将旧数组中的数据,迁移到新数组。
    将HashMap的底层数组指向新数组。
    重新计算threshold。
    数据迁移?
    遍历旧数组。
    针对每个数组成员,从头开始遍历链表。
    针对每个链表节点,重新计算哈希值,以及该哈希值在新数组中的位置。
    将当前节点的next,指向新数组中该位置的节点。
    将新数组中该位置,指向当前节点。
    以上过程造成了两种后果:
    若重新哈希后没有冲突,直接存放为数组元素;
    若重新哈希后仍有冲突,新链表的顺序与原来相反。

    9、HashMap.get(key)多线程环境下,CPU飙到100%。
    根本原因,在HashMap扩容,数据迁移的时候,出现了环状链表结构。
    相关代码:
           void transfer(Entry[] newTable)
           {
                  Entry[] src = table;
                  int newCapacity = newTable.length;
                  // 下面这段代码的意思是:
                  // 从OldTable里摘一个元素出来,然后放到NewTable中
                  for (int j = 0; j < src.length; j++) {
                         Entry<K,V> e = src[j];
                         if (e != null) {
                                src[j] = null;
                                do {
                                       Entry<K,V> next = e.next; // 线程一在这里挂起
                                       int i = indexFor(e.hash, newCapacity);
                                       e.next = newTable[i];
                                       newTable[i] = e;
                                       e = next;
                                } while (e != null);
                         }
                  }
           }
    形成过程:
    为了便于分析问题,假定链表长度为2,头结点为node1,尾节点为node2。
    第一阶段->线程1>第1次循环>上半场
    创建数组a,遍历旧链表,第1个语句,之后当前线程挂起;
    此时情况,持有两个引用,e指向头结点(node1),next指向尾节点(node2),即e.next。
    第二阶段->线程2。
    创建数组b,遍历旧链表,完毕之后,新链表的顺序与原来相反。
    此时情况,node1变成了尾节点,node2变成了头结点。
    b[i]=node2,node2.next=node1,node1.next=null。
    第三阶段>线程1>第1次循环>下半场
    初始情况,e指向尾节点(node1),next指向头结点(node2)。
    第2个语句,计算尾节点e(node1)的数组下标i。
    第3个语句,尾节点e(node1)的next(e.next),指向a[i](此时为null)。
    第4个语句,a[i]指向尾节点e(node1)。
    第5个语句,e指向头结点next(node2)。
    此时情况,a[i]指向尾节点(node1),e指向头结点(node2),next也指向头结点(node2)。
    a[i]=node1,node1.next=null,b[i]=node2,node2.next=node1。
    第四阶段>线程1>第2次循环
    第1个语句,next指向尾节点(node1)。
    第2个语句,计算头结点e(node2)的数组下标i。
    第3个语句,头结点e(node2)的next(e.next),指向a[i](此时为node1),相当于node2.next=node1。(没有任何效果)
    第4个语句,a[i]指向头结点e(node2)。
    第5个语句,e指向尾节点next(node1)。
    此时情况,a[i]指向头节点(node2),e指向尾结点(node1)。
    a[i]=node2,node2.next=node1,node1.next=null,b[i]=node2。
    第五阶段>线程1>第3次循环
    第1个语句,next指向null。
    第2个语句,计算尾结点e(node1)的数组下标i。
    第3个语句,尾结点e(node1)的next(e.next),指向a[i](此时为node2),即node1.next=node2。
    总结。
    第四阶段,node2.next=node1。
    第五阶段,node1.next=node2。
    由此环路形成。

    10、HashSet.toString()多线程环境下,出现内存溢出。
    两个原因,
    HashSet基于HashMap实现,多线程环境下,出现了环状链表结构。
    HashSet.toString拼装StringBuilder时,遍历上述结构,陷入死循环。

    11、两阶段提交和三阶段提交。
    两阶段提交:
    第一阶段,投票+预提交(各就各位+预备);
    第二阶段,提交(走你)。
    三阶段提交:
    第一阶段,投票(各就各位);
    第二阶段,预提交(预备);
    第三阶段,提交(走你)。

    12、JDK6、JDK7、JDK8,常量池、方法区(永久代)、元空间。
    JDK6,常量池位于方法区,又叫永久代,配置参数-XX:PermSize、-XX:MaxPermSize
    JDK7,常量池位于堆内存。
    JDK8,彻底去掉了方法区,取而代之的是元空间,配置参数-XX:MetaspaceSize、-XX:MaxMetaspaceSize

    13、TLAB是什么?它位于堆内存,还是栈内存?

    14、对象可以直接分配在栈内存吗?

    15、JVM线上问题分类排查。
    CPU问题。
    死循环:查看系统负载,排查方法:top/top H/top -Hp pid。
    线程阻塞:查看线程栈,排查方法:jstack pid。
    频繁GC:查看GC情况,排查方法:jstat -gcutil pid。
    内存类问题。
    堆内存:OutOfMemoryError:java heap space,排查方法:集合、缓存、多线程、大对象、jmap –heap、jmap –histo、jmap -dump:format=b,file=xxx.hprof。
    栈内存:
    StackOverflowError,排查方法:栈深度过大、递归死循环等;
    OutOfMemoryError:unable to create new native thread,排查方法:线程数量过多、物理内存不足、超过系统限制
    方法区:OutOfMemoryError:PermGen space,排查方法:XX:MaxPermSize、class/jar过多、重复加载、动态代理。
    直接内存:OutOfMemoryError,排查方法:Java Native Interface、java.nio.DirectByteBuffer等
    IO类问题。
    磁盘IO。
    网络IO。

    16、栈内存什么情况下StackOverflowError,什么情况下OutOfMemoryError?
    单个线程的栈帧太大,抛出StackOverflowError。
    创建的线程数量过多,抛出OutOfMemoryError。

    17、强引用、软应用、弱引用、虚引用。
    强引用,诸如Object obj = new Object(),永远不会垃圾回收。
    软引用,诸如SoftReference reference = new SoftReference(obj),在内存溢出之前,进行二次回收。
    弱引用,诸如WeakReference reference = new WeakReference(obj),下一次垃圾回收时进行回收。
    虚引用,诸如PhantomReference reference = new PhantomReference(obj),对垃圾回收无任何影响,也无法通过虚引用取得对象,它存在的唯一目的,就是对象被回收时,收到一个系统通知。

    18、判定对象是否应该被回收,引用计数器 OR 可达性分析?
    答案是可达性分析,因为引用计数器无法解决循环引用的问题。
    可达性分析是指,从对象到GC Roots没有任何引用链相连接。
    GC Roots包含,虚拟机栈和本地方法栈中引用的对象,方法区中静态成员变量和常量引用的对象。

    19、对象被判定为不可达后,是否非死不可?
    答案是NO。
    标记对象是否有必要执行finalize方法(对象覆盖了finalize方法,且未执行过,表示有必要执行)
    若无必要执行,则进行回收。
    创建的线程数量过多,抛出OutOfMemoryError。
    若有必要执行,则登记F-Queue队列,启动Finalizer线程,执行finalize方法。
    若finalize方法中把对象复活,则对象不进行回收,否则进行回收。
    以上复活的机会只有一次,下一次finalize方法不再执行

    20、CPU负载飙高甚至达到100%,如何排查?
           第一步,找到CPU负载最高的进程pid:top/top -c
    第二步,找到CPU负载最高的线程pid:top -Hp pid。
    第三步,线程pid转换为十六进制:printf ‘%x\n’ pid。
    第四步,找到Java堆栈信息:jstack 进程pid | grep 线程pid十六进制 -C10 –color。
    第五步,定位到线程信息和问题代码。

    21、JVM参数-XX:+DisableExplicitGC、-XX:+ExplicitGCInvokesConcurrent的含义。
    -XX:+DisableExplicitGC,禁用System.gc()显式调用GC。
    -XX:+ExplicitGCInvokesConcurrent,启用并行的FullGC。
    常与堆外直接内存配合使用。(?)

    22、什么是堆外内存?
    狭义的堆外内存,通过java.nio.DirectByteBuffer分配的内存,通过JVM参数-XX:MaxDirectMemorySize指定大小
    若未指定,则默认=新生代+老生代-survivor=-Xmx-survivor
    广义的堆外内存,堆之外的所有内存,包含程序计数器、虚拟机栈、本地方法栈、直接内存。
    注意:对于HotSpot,Java6/Java7的方法区又叫永久代,与堆内存一起分配和管理,因此属于堆内存的一部分。
    对于频繁操作内存,只需临时存储的场景,建议使用堆外内存,并且做成缓冲池,便于重复利用。

    23、使用堆外内存,为什么显式调用GC?
    DirectByteBuffer对象创建的时候,关联PhantomReference用于对象跟踪,创建sun.misc.Cleaner用于对象回收。(Unsafe的free接口。)
    GC过程中,若发现对象只被PhantomReference引用,则该引用登记到java.lang.ref.Reference.pending队列
    GC完毕后,通知ReferenceHandler守护线程进行后置处理,若pending队列为空,则线程阻塞,否则进行遍历
    若Reference类型为Cleaner,则调用Cleaner.clean(),否则登记ReferenceQueue队列,可以用于对象的复活。(类似于finalizer)

    24、JVM监控小工具jstat。
          
     jstat -gc pid——S0C、S1C、S0U、S1U、EC、EU、OC、OU、PC、PU、YGC、YGCT、FGC、FGCT、GCT
           jstat -gcutil pid——S0、S1、E、O、P、YGC、YGCT、FGC、FGCT、GCT。
           jstat -gccapacity pid。
    jstat -gccause pid。

    25、-XX:+DisableExplicitGC,关闭System.gc(),亦即禁止手动触发STW的FGC。

    26、-XX:+ExplicitGCInvokesConcurrent,YGC+部分OGC,性能比Full GC要好,STW时间变短,后台回收。

    27、

  • 分布式序列号生成方案-待完善

    一、概述。

    1、应用场景。

    消息标识。

    订单标识。

    运单标识。

    帖子标识。

    2、核心问题。

    保证全局唯一。

    满足一定规则。

    位数固定,统一前缀,或者后缀。

    趋势有序,时间相关。

    有校验位,防止推断。

    高性能、高可用、吞吐量、易用性。

    二、依赖数据库(MySQL)。

    1、单点单次。

    2、单点批量。

    3、主备批量。

    优点:

    缺点:

    优化:

    三、依赖缓存(Redis/JimDB)。

    INCR、INCRBY、DECR、DECRBY。

    优点:

    缺点:

    优化:

    四、依赖ZooKeeper ZNode。

    优点:

    缺点:

    优化:

    五、UUID及其变种。

    折半UUID。

    优点:

    缺点:

    优化:

    六、snowflake算法。。

    优点:

    缺点:

    优化:

    七、总结。

    自动故障迁移

    一致性哈希

    CPU寄存器

    L1高速缓存

    内存

    磁盘

    外部缓存

  • JVM性能调优-收集

    1、JVM源码分析之SystemGC完全解读
    http://mp.weixin.qq.com/s/V1Y6DIoscTuv7RVlIZgVpw

    2、JVM源码分析之堆外内存完全解读
    http://mp.weixin.qq.com/s/WgQkXxBJDF7QdTHFFGVypg

    3、JVM源码分析之Object.wait/notify(All)完全解读
    http://mp.weixin.qq.com/s/4oCEWVrs67aONxEgMaOVFg

    4、JDK的sql设计不合理导致的驱动类初始化死锁问题
    http://mp.weixin.qq.com/s/XVXEZK71ZKGbIgvCGl7tIg

    5、JVM源码分析之FinalReference完全解读
    http://mp.weixin.qq.com/s/-ER8S28tb17-f51S_NgADQ

    6、如何定位消耗CPU最多的线程
    http://mp.weixin.qq.com/s/c-KuGjI_VH1dTxIWtxZJEg

    7、不可逆的类初始化过程
    http://mp.weixin.qq.com/s/HK5JsmGjvOe_93TwlmZNdg

    8、JVM源码分析之javaagent原理完全解读
    http://mp.weixin.qq.com/s/OLeWL70E0qFACzw5Ri8GTw

    9、JDK8在泛型类型推导上的变化
    http://mp.weixin.qq.com/s/lz5RyWUmneCDpGSqyVLy9Q

    10、JVM源码分析之自定义类加载器如何拉长YGC
    http://mp.weixin.qq.com/s/fiuB2f3Gv5XDka0rrP2eAw

    11、进程物理内存远大于Xmx的问题分析
    http://mp.weixin.qq.com/s/XJ1xXz8dtMv1DVcZVf73GA

    12、JVM源码分析之Attach机制实现完全解读
    http://mp.weixin.qq.com/s/-ER8S28tb17-f51S_NgADQ

    13、JVM源码分析之栈溢出完全解读
    http://mp.weixin.qq.com/s/1d8W-eyzsnGDr9-uSfag6Q

    14、JVM源码分析之JDK8下的僵尸(无法回收)类加载器
    http://mp.weixin.qq.com/s/mbLuRjfw56wglBmOdtVskQ

    15、消失的死锁
    http://mp.weixin.qq.com/s/KOzt6kOXH3MapR3Ug43gvw

    16、YGC前后新生代变大
    http://mp.weixin.qq.com/s/YigddMVvRj7nO1xAZOXa1Q

    17、诡异GC问题收集
    http://mp.weixin.qq.com/s/rX6mDmZDQ9SWtze4F0hvvQ

    18、JVM源码分析之jstat工具原理完全解读
    http://mp.weixin.qq.com/s/gCE9eXbtMuze3jhuRm1YXA

    19、JVM源码分析之不可控的堆外内存
    http://mp.weixin.qq.com/s/MgMYy-K0G753-_qCsqFyVA

    20、JVM源码分析之临门一脚的OutOfMemoryError完全解读
    http://mp.weixin.qq.com/s/M8TFyVzS5MvwL2M639m3Bg

    21、JVM源码分析之Metaspace解密
    http://mp.weixin.qq.com/s/SsXbRvtvawKDHstFpU4uog

    22、JVM源码分析之不保证顺序的Class.getMethods
    http://mp.weixin.qq.com/s/XrAD1Q09mJ-95OXI2KaS9Q

    23、JVM源码分析之String.intern()导致的YGC不断变长
    http://mp.weixin.qq.com/s/RtIZd4zaa-UkxoxtUsXd8Q

    24、JVM源码分析之自定义类加载器如何拉长YGC
    http://mp.weixin.qq.com/s/rsYm1WTMKv5V2SRHmwSdlw

    25、Java的时间为何从1970年1月1日开始
    http://mp.weixin.qq.com/s/mUTOMe4rXhKWp5sOHZK9Xw

    26、JVM源码分析之System.currentTimeMillis及nanoTime原理详解
    http://mp.weixin.qq.com/s/w37FXrVjQL36i8m_WLpf_w

    27、JVM源码分析之一个Java进程究竟能创建多少线程
    http://mp.weixin.qq.com/s/K8Y1wOloEwj1yQGEf7TnZQ

    28、JVM源码分析之警惕存在内存泄漏风险的FinalReference(增强版)
    https://mp.weixin.qq.com/s/igboi4xvjT4hTEFqZjLxEA

    29、假笨说-从X86指令深扒JVM的位移操作
    http://mp.weixin.qq.com/s/Mbg4y8fZdE4Mi37uz8-czA

    30、假笨说-我是如何走上JVM这条贼船的
    http://mp.weixin.qq.com/s/u7AWMDORvYa1TV4La18ObQ

    31、假笨说-从一起GC血案谈到反射原理
    http://mp.weixin.qq.com/s/5H6UHcP6kvR2X5hTj_SBjA

    32、来云栖社区聊聊Java开发者规范吧
    https://mp.weixin.qq.com/s/9kFI8WDxreHszt0Xa7fl4g

    33、假笨说-谨防JDK8重复类定义造成的内存泄漏
    https://mp.weixin.qq.com/s/3sb_ovHhhTXTid3G5iZUew

    34、假笨说-类初始化死锁导致线程被打爆!打爆!爆!
    http://mp.weixin.qq.com/s/UwEO8hFq-EL3a_VjMRydkA

    35、假笨说-又抓了一个导致频繁GC的鬼–数组动态扩容
    http://mp.weixin.qq.com/s/HKdpmmvJKq45QZdV4Q2cYQ

    36、假笨说-关于数组动态扩容导致频繁GC的问题,我还有话说
    http://mp.weixin.qq.com/s/GuPpF5LWydwZVnEiz6KoHw

    37、假笨说-查JVM参数就找JVMPocket(JVM口袋)小程序吧
    http://mp.weixin.qq.com/s/XJrH8lN6N0i7juf2AKZmmA

    38、假笨说-给JVMPocket提建议,赠您JVM的好书,可好?
    http://mp.weixin.qq.com/s/sNB42zc60oRAZL9Y54-e6g

    39、假笨说-警惕大量类加载器的创建导致诡异的Full GC
    http://mp.weixin.qq.com/s/qgpMMR8-493-Y9uwWiMdRg

    40、揪出一个导致GC慢慢变长的JVM设计缺陷
    http://mp.weixin.qq.com/s/m0YpJuHB3pkrvUylYbe5eg

    41、假笨说-关于内存溢出,咱再聊点有意思的
    http://mp.weixin.qq.com/s/ET8C8VrbcCLFSipCLKgvYA

    42、假笨说-JVM参数,我准备做些分享,你想听吗
    http://mp.weixin.qq.com/s/OhalT8Y4MgOWCiNA3y9iQg

    43、假笨说参数-对象晋升相关的MaxTenuringThreshold
    http://mp.weixin.qq.com/s/9NULcNlV7G4Ssgn5FSFzbg

    44、假笨说参数-GC日志其实也支持滚动输出的
    http://mp.weixin.qq.com/s/aGT31AQyH7NRqnRGArE2eg

     

  • lsof查看端口被谁占用

    使用 lsof 查找打开的文件

    通过查看打开的文件,了解更多关于系统的信息。了解应用程序打开了哪些文件或者哪个应用程序打开了特定的文件,作为系统管理员,这将使得您能够作出更好的决策。例如,您不应该卸载具有打开文件的文件系统。使用 lsof,您可以检查打开的文件,并根据需要在卸载之前中止相应的进程。同样地,如果您发现了一个未知的文件,那么可以找出到底是哪个应用程序打开了这个文件。

    在 UNIX® 环境中,文件无处不在,这便产生了一句格言:“任何事物都是文件”。通过文件不仅仅可以访问常规数据,通常还可以访问网络连接和硬件。在有些情况下,当您使用 ls 请求目录清单时,将出现相应的条目。在其他情况下,如传输控制协议 (TCP) 和用户数据报协议 (UDP) 套接字,不存在相应的目录清单。但是在后台为该应用程序分配了一个文件描述符,无论这个文件的本质如何,该文件描述符为应用程序与基础操作系统之间的交互提供了通用接口。

    因为应用程序打开文件的描述符列表提供了大量关于这个应用程序本身的信息,所以能够查看这个列表将是很有帮助的。完成这项任务的实用程序称为 lsof,它对应于“list open files”(列出打开的文件)。几乎在每个 UNIX 版本中都有这个实用程序,但奇怪的是,大多数供应商并没有将其包含在操作系统的初始安装中。要获取更多关于 lsof 的信息,请参见参考资料部分。

    lsof 简介

    只需输入 lsof 就可以生成大量的信息,如清单 1 所示。因为 lsof 需要访问核心内存和各种文件,所以必须以 root 用户的身份运行它才能够充分地发挥其功能。

    清单 1. lsof 的示例输出
    bash-3.00# lsof 
    COMMAND    PID   USER   FD   TYPE        DEVICE SIZE/OFF      NODE NAME
    sched        0   root  cwd   VDIR         136,8     1024         2 /
    init         1   root  cwd   VDIR         136,8     1024         2 /
    init         1   root  txt   VREG         136,8    49016      1655 /sbin/init
    init         1   root  txt   VREG         136,8    51084      3185 /lib/libuutil.so.1
    vi        2013   root    3u  VREG         136,8        0      8501 /var/tmp/ExXDaO7d
    ...

    每行显示一个打开的文件,除非另外指定,否则将显示所有进程打开的所有文件。CommandPID 和 User 列分别表示进程的名称、进程标识符 (PID) 和所有者名称。DeviceSIZE/OFFNode 和 Name 列涉及到文件本身的信息,分别表示指定磁盘的名称、文件的大小、索引节点(文件在磁盘上的标识)和该文件的确切名称。根据 UNIX 版本的不同,可能将文件的大小报告为应用程序在文件中进行读取的当前位置(偏移量)。清单 1 来自一台可以报告该信息的 Sun Solaris 10 计算机,而 Linux® 没有这个功能。

    FD 和 Type 列的含义最为模糊,它们提供了关于文件如何使用的更多信息。FD 列表示文件描述符,应用程序通过文件描述符识别该文件。Type 列提供了关于文件格式的更多描述。我们来具体研究一下文件描述符列,清单 1 中出现了三种不同的值。cwd 值表示应用程序的当前工作目录,这是该应用程序启动的目录,除非它本身对这个目录进行更改。txt 类型的文件是程序代码,如应用程序二进制文件本身或共享库,再比如本示例的列表中显示的 init 程序。最后,数值表示应用程序的文件描述符,这是打开该文件时返回的一个整数。在清单 1 输出的最后一行中,您可以看到用户正在使用 vi 编辑 /var/tmp/ExXDaO7d,其文件描述符为 3。u 表示该文件被打开并处于读取/写入模式,而不是只读 (r) 或只写 (w) 模式。有一点不是很重要但却很有帮助,初始打开每个应用程序时,都具有三个文件描述符,从 0 到 2,分别表示标准输入、输出和错误流。正因为如此,大多数应用程序所打开的文件的 FD 都是从 3 开始。

    与 FD 列相比,Type 列则比较直观。根据具体操作系统的不同,您会发现将文件和目录称为 REG 和 DIR(在 Solaris 中,称为 VREG 和 VDIR)。其他可能的取值为 CHR 和 BLK,分别表示字符和块设备;或者 UNIXFIFO 和 IPv4,分别表示 UNIX 域套接字、先进先出 (FIFO) 队列和网际协议 (IP) 套接字。

    转到 /proc 目录

    尽管与使用 lsof 没有什么直接的关系,但对 /proc 目录进行简要的介绍是有必要的。/proc 是一个目录,其中包含了反映内核和进程树的各种文件。这些文件和目录并不存在于磁盘中,因此当您对这些文件进行读取和写入时,实际上是在从操作系统本身获取相关信息。大多数与 lsof相关的信息都存储于以进程的 PID 命名的目录中,所以 /proc/1234 中包含的是 PID 为 1234 的进程的信息。

    在 /proc 目录的每个进程目录中存在着各种文件,它们可以使得应用程序简单地了解进程的内存空间、文件描述符列表、指向磁盘上的文件的符号链接和其他系统信息。lsof 实用程序使用该信息和其他关于内核内部状态的信息来产生其输出。稍后我将把 lsof 的输出与 /proc 目录中的信息联系起来。

    常见用法

    前面,我向您介绍了如何简单地运行不带任何参数的 lsof,以便显示关于每个进程所打开的文件的信息。本文余下的部分将重点关注如何使用 lsof 来显示所需的信息以及如何正确地对其进行解释。

    查找应用程序打开的文件

    lsof 常见的用法是查找应用程序打开的文件的名称和数目。您可能想尝试找出某个特定应用程序将日志数据记录到何处,或者正在跟踪某个问题。例如,UNIX 限制了进程能够打开文件的数目。通常这个数值很大,所以不会产生问题,并且在需要时,应用程序可以请求更大的值(直到某个上限)。如果您怀疑应用程序耗尽了文件描述符,那么可以使用 lsof 统计打开的文件数目,以进行验证。

    要指定单个进程,可以使用 -p 参数,后面加上该进程的 PID。因为这样做不仅会返回该应用程序所打开的文件,还会返回共享库和代码,所以通常需要对输出进行筛选。要完成此任务,可以使用 -d 标志根据 FD 列进行筛选,使用 -a 标志表示两个参数都必须满足 (AND)。如果没有 -a标志,缺省的情况是显示匹配任何一个参数 (OR) 的文件。清单 2 显示了 sendmail 进程打开的文件,并使用 txt 对这些文件进行筛选。

    清单 2. 带有 PID 筛选器并进行 txt 文件描述符筛选的 lsof 输出
    sh-3.00# lsof -a -p 605 -d ^txt
    COMMAND  PID USER   FD   TYPE  DEVICE SIZE/OFF     NODE NAME
    sendmail 605 root  cwd   VDIR  136,8     1024    23554 /var/spool/mqueue
    sendmail 605 root    0r  VCHR  13,2            6815752 /devices/pseudo/mm@0:null
    sendmail 605 root    1w  VCHR  13,2            6815752 /devices/pseudo/mm@0:null
    sendmail 605 root    2w  VCHR  13,2            6815752 /devices/pseudo/mm@0:null
    sendmail 605 root    3r  DOOR             0t0       58
    		/var/run/name_service_door(door to nscd[81]) (FA:->0x30002b156c0)
    sendmail 605 root    4w  VCHR  21,0           11010052 
    						/devices/pseudo/log@0:conslog->LOG
    sendmail 605 root    5u  IPv4 0x300010ea640      0t0      TCP *:smtp (LISTEN)
    sendmail 605 root    6u  IPv6 0x3000431c180      0t0      TCP *:smtp (LISTEN)
    sendmail 605 root    7u  IPv4 0x300046d39c0      0t0      TCP *:submission (LISTEN)
    sendmail 605 root    8wW VREG         281,3       32  8778600 /var/run/sendmail.pid

    清单 2 为 lsof 指定了三个参数。第一个是 -a,它表示当所有的参数都为真时,才显示这个文件。第二个参数是 -p 605,它限制仅输出 PID 为 605 的进程,可以通过 ps 命令获取这个信息。最后一个参数 -d ^txt,它表示筛选出其中 txt 类型的记录(脱字符号 [^] 表示排除)。

    清单 2 的输出提供了关于进程行为的信息。如 cwd 行所示,该应用程序的工作目录为 /var/spool/mqueue。文件描述符 0、1 和 2 分配给了 /dev/null(Solaris 大量使用符号链接,所以这里显示了相应的伪设备)。FD 3 是一个 Solaris 门(高速远程过程调用 (RPC) 接口),以只读模式打开。FD 4 中的内容比较有趣,因为它是一个字符设备的只读句柄,实质上是 /dev/log。从这个文件中,您可以收集该应用程序向 UNIX syslog 守护进程进行的记录,所以 /etc/syslog.conf 规定了日志文件的位置。

    作为一个网络应用程序,sendmail 对网络端口进行监听。文件描述符 5、6 和 7 可以告诉您,该应用程序正以 IPv4 和 IPv6 模式监听简单邮件传输协议 (SMTP) 端口,并以 IPv4 模式监听提交端口。最后一个文件描述符是只写的,并且指向 /var/run/sendmail.pid。FD 列中的大写 W 表示该应用程序具有对整个文件的写锁。该文件用于确保每次只能打开一个应用程序实例。

    查找打开某个文件的应用程序

    在其他情况下,您有一个文件或目录,并且需要知道哪个应用程序控制了该文件(打开了该文件)。清单 2 显示了由 sendmail 进程打开了 /var/run/sendmail.pid。如果您不知道这个信息,那么在给定文件名的情况下,lsof 可以提供该信息。清单 3 显示了相应的输出。

    清单 3. 要求 lsof 显示关于某个文件的信息
    bash-3.00# lsof /var/run/sendmail.pid
    COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF    NODE NAME
    sendmail 605 root    8wW VREG  281,3       32 8778600 /var/run/sendmail.pid

    正如输出所示,进程 sendmail(PID 为 605)控制了文件 /var/run/sendmail.pid,并且通过排它锁打开该文件以便进行写入。如果出于某种原因,您需要删除这个文件,那么正确的做法是中止该进程,而不是直接删除这个文件。否则,这个守护进程下次可能无法正常启动,或者可能稍后会启动另一个实例,从而导致争用。

    有时您只知道在文件系统的某处打开了文件。在卸载文件系统时,如果该文件系统中有任何打开的文件,那么操作将会失败。通过指定装入点的名称,您可以使用 lsof 显示一个文件系统中所有打开的文件。清单 4 显示了如何尝试卸载 /export/home,然后使用 lsof 找出谁在使用该文件系统。

    清单 4. 使用 lsof 找出谁在使用文件系统
    bash-3.00# umount /export/home
    umount: /export/home busy
    bash-3.00# lsof /export/home
    COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
    bash    1943 root  cwd   VDIR  136,7     1024    4 /export/home/sean
    bash    2970 sean  cwd   VDIR  136,7     1024    4 /export/home/sean
    ct      3030 sean  cwd   VDIR  136,7     1024    4 /export/home/sean
    ct      3030 sean    1w  VREG  136,7        0   25 /export/home/sean/output

    在这个示例中,用户 sean 正在其 home 目录中进行一些操作。有两个 bash(一种 Shell)实例正在运行,并且当前目录设置为 sean 的 home 目录。还有一个名为 ct 的应用程序正运行于相同的目录,并且其标准输出(文件描述符 1)重定向到一个名为 output 的文件。要成功地卸载 /export/home,应该在通知用户以确保情况正常之后,中止这些进程。

    这个示例说明了应用程序的当前工作目录非常重要,因为它仍保持着文件资源,并且可以防止文件系统被卸载。这就是为什么大部分守护进程(后台进程)将它们的目录更改为根目录、或服务特定的目录(如 sendmail 示例中的 /var/spool/mqueue)的原因,以避免该守护进程阻止卸载不相关的文件系统。如果 sendmail 从 /export/home/sean 目录启动,并且没有将其目录更改为 /var/spool/mqueue,那么在卸载 /export/home 前必须中止它。

    如果您对非装入点目录中打开的文件感兴趣,那么必须通过 +d 或 +D 指定该目录的名称,具体使用其中的哪一个标志取决于您需要递归到子目录(+D)或者不需要递归到子目录(+d)。例如,要查看 /export/home/sean 中所有打开的文件,可以使用 lsof +D /export/home/sean。在前面的示例中,相关的目录是一个装入点,而这里与前面的示例存在细微的差别,并且限制了 lsof 和内核之间的交互。这还会引起潜在的问题,即 lsof /export/home 与 lsof /export/home/(请注意尾部的斜杠)有所区别。第一种方式可以正常工作,因为它指向了装入点。第二种方式不会生成任何输出,因为它指向了目录。如果您在 Shell 中使用 Tab 键自动完成命令,那么可能碰到这个问题,其中会帮助您添加结尾的斜杠。在这种情况下,您可以删除这个斜杠或者使用 +D 指定目录。前者是首选的方法,因为与指定任意的目录相比,其执行速度更快。

    不常见的用法

    在前面的部分中,我们研究了 lsof 的基本用法,即显示打开的文件和控制它们的进程之间的关系。当您想对系统进行一些烦琐的操作,而又不希望破坏别人重要的文档时,这种方法很有帮助。您还可以使用相同的方法执行一些高难度的 UNIX 操作。

    恢复删除的文件

    当 UNIX 计算机受到入侵时,常见的情况是日志文件被删除,以掩盖攻击者的踪迹。管理错误也可能导致意外删除重要的文件,比如在清理旧日志时,意外地删除了数据库的活动事务日志。有时可以恢复这些文件,并且 lsof 可以为您提供帮助。

    当进程打开了某个文件时,只要该进程保持打开该文件,即使将其删除,它依然存在于磁盘中。这意味着,进程并不知道文件已经被删除,它仍然可以向打开该文件时提供给它的文件描述符进行读取和写入。除了该进程之外,这个文件是不可见的,因为已经删除了其相应的目录条目。

    前面曾在转到 /proc 目录部分中说过,通过在适当的目录中进行查找,您可以访问进程的文件描述符。在随后的内容中,您看到了 lsof 可以显示进程的文件描述符和相关的文件名。您能明白我的意思吗?

    但愿它真的这么简单!当您向 lsof 传递文件名时,比如在 lsof /file/I/deleted 中,它首先使用 stat() 系统调用获得有关该文件的信息,不幸的是,这个文件已经被删除。在不同的操作系统中,lsof 可能可以从核心内存中捕获该文件的名称。清单 5 显示了一个 Linux 系统,其中意外地删除了 Apache 日志,我正使用 grep 工具查找是否有人打开了该文件。

    清单 5. 在 Linux 中使用 lsof 查找删除的文件
    # lsof | grep error_log
    httpd      2452     root    2w      REG       33,2      499    3090660
    					/var/log/httpd/error_log (deleted)
    httpd      2452     root    7w      REG       33,2      499    3090660
    					/var/log/httpd/error_log (deleted)
    ... more httpd processes ...

    在这个示例中,您可以看到 PID 2452 打开文件的文件描述符为 2(标准错误)和 7。因此,可以在 /proc/2452/fd/7 中查看相应的信息,如清单 6 所示。

    清单 6. 通过 /proc 查找删除的文件
    # cat /proc/2452/fd/7
    [Sun Apr 30 04:02:48 2006] [notice] Digest: generating secret for digest authentication
    [Sun Apr 30 04:02:48 2006] [notice] Digest: done
    [Sun Apr 30 04:02:48 2006] [notice] LDAP: Built with OpenLDAP LDAP SDK

    Linux 的优点在于,它保存了文件的名称,甚至可以告诉我们它已经被删除。在遭到破坏的系统中查找相关内容时,这是非常有用的内容,因为攻击者通常会删除日志以隐藏他们的踪迹。Solaris 并不提供这些信息。然而,我们知道 httpd 守护进程使用了 error_log 文件,所以可以使用 ps 命令找到这个 PID,然后可以查看这个守护进程打开的所有文件。

    清单 7. 在 Solaris 中查找删除的文件
    # lsof -a -p 8663 -d ^txt
    COMMAND  PID   USER   FD   TYPE        DEVICE SIZE/OFF    NODE NAME
    httpd   8663 nobody  cwd   VDIR         136,8     1024       2 /
    httpd   8663 nobody    0r  VCHR          13,2          6815752 /devices/pseudo/mm@0:null
    httpd   8663 nobody    1w  VCHR          13,2          6815752 /devices/pseudo/mm@0:null
    httpd   8663 nobody    2w  VREG         136,8      185  145465 / (/dev/dsk/c0t0d0s0)
    httpd   8663 nobody    4r  DOOR                    0t0      58 /var/run/name_service_door
    						(door to nscd[81]) (FA:->0x30002b156c0)
    httpd   8663 nobody   15w  VREG         136,8      185  145465 / (/dev/dsk/c0t0d0s0)
    httpd   8663 nobody   16u  IPv4 0x300046d27c0      0t0     TCP *:80 (LISTEN)
    httpd   8663 nobody   17w  VREG         136,8        0  145466 
                                                              /var/apache/logs/access_log
    httpd   8663 nobody   18w  VREG         281,3        0 9518013 /var/run (swap)

    我使用 -a 和 -d 参数对输出进行筛选,以排除代码程序段,因为我知道需要查找的是哪些文件。Name 列显示出,其中的两个文件(FD 2 和 15)使用磁盘名代替了文件名,并且它们的类型为 VREG(常规文件)。在 Solaris 中,删除的文件将显示文件所在的磁盘的名称。通过这个线索,就可以知道该 FD 指向一个删除的文件。实际上,查看 /proc/8663/fd/15 就可以得到所要查找的数据。

    如果可以通过文件描述符查看相应的数据,那么您就可以使用 I/O 重定向将其复制到文件中,如 cat /proc/8663/fd/15 > /tmp/error_log 。此时,您可以中止该守护进程(这将删除 FD,从而删除相应的文件),将这个临时文件复制到所需的位置,然后重新启动该守护进程。

    对于许多应用程序,尤其是日志文件和数据库,这种恢复删除文件的方法非常有用。正如您所看到的,有些操作系统(以及不同版本的 lsof)比其他的系统更容易查找相应的数据。

    查找网络连接

    网络连接也是文件,这意味着可以使用 lsof 获得关于它们的信息。您曾在清单 2 中看到过这样的示例。该示例假设您已经知道 PID,但是有时候并非如此。如果您只知道相应的端口,那么可以使用 -i 参数利用套接字信息进行搜索。清单 8 显示了对 TCP 端口 25 的搜索。

    清单 8. 查找监听端口 25 的进程
    # lsof -i :25
    COMMAND  PID USER   FD   TYPE        DEVICE SIZE/OFF NODE NAME
    sendmail 605 root    5u  IPv4 0x300010ea640      0t0  TCP *:smtp (LISTEN)
    sendmail 605 root    6u  IPv6 0x3000431c180      0t0  TCP *:smtp (LISTEN)

    需要以 protocol:@ip:port 的形式向 lsof 实用程序传递相关信息,其中的 protocol 为 TCP 或 UDP(可以使用 4 或 6 作为前缀,表示 IP 的版本),IP 为可解析的名称或 IP 地址,而 port 为数字或表示该服务的名称(来自 /etc/services)。需要一个或多个元素(端口、IP、协议)。在清单 8 中,:25 表示端口 25。输出显示,进程 605 正在使用 IPv6 和 IPv4 监听端口 25。如果您对 IPv4 不感兴趣,那么可以将筛选器改为 6:25,以表示监听端口 25 的 IPv6 套接字,或者直接使用 6 表示所有的 IPv6 连接。

    除了显示出这些守护进程正在监听的对象,lsof 还可以发现发生的连接,同样是使用 -i 参数。清单 9 显示了搜索与 192.168.1.10 之间的所有连接。

    清单 9. 搜索活动的连接
    # lsof -i @192.168.1.10
    

     

     

  • 为应用选择和创建最佳索引,加速数据读取

    由于SQL问题导致的数据库故障层出不穷,索引问题是SQL问题中出现频率最高的,常见的索引问题包括:无索引,隐式转换,索引创建不合理。

    当数据库中出现访问表的SQL没创建索引导致全表扫描,如果表的数据量很大扫描大量的数据,执行效率过慢,占用数据库连接,连接数堆积很快达到数据库的最大连接数设置,新的应用请求将会被拒绝导致故障发生。

    隐式转换是指SQL查询条件中的传入值与对应字段的数据定义不一致导致索引无法使用。常见隐式转换如字段的表结构定义为字符类型,但SQL传入值为数字;或者是字段定义collation为区分大小写,在多表关联的场景下,其表的关联字段大小写敏感定义各不相同。隐式转换会导致索引无法使用,进而出现上述慢SQL堆积数据库连接数跑满的情况。

    索引使用策略及优化

    创建索引

    • 在经常查询而不经常增删改操作的字段加索引。
    • order by与group by后应直接使用字段,而且字段应该是索引字段。
    • 一个表上的索引不应该超过6个。
    • 索引字段的长度固定,且长度较短。
    • 索引字段重复不能过多。
    • 在过滤性高的字段上加索引。

    使用索引注意事项

    • 使用like关键字时,前置%会导致索引失效。
    • 使用null值会被自动从索引中排除,索引一般不会建立在有空值的列上。
    • 使用or关键字时,or左右字段如果存在一个没有索引,有索引字段也会失效。
    • 使用!=操作符时,将放弃使用索引。因为范围不确定,使用索引效率不高,会被引擎自动改为全表扫描。
    • 不要在索引字段进行运算。
    • 在使用复合索引时,最左前缀原则,查询时必须使用索引的第一个字段,否则索引失效;并且应尽量让字段顺序与索引顺序一致。
    • 避免隐式转换,定义的数据类型与传入的数据类型保持一致。

    无索引案例

    无索引案例一

    1. 查看表结构。
      1. mysql> show create table customers;
      1. CREATE TABLE `customers` (
      2.  `cust_id` int(11) NOT NULL AUTO_INCREMENT,
      3.  `cust_name` char(50) NOT NULL,
      4.  `cust_address` char(50) DEFAULT NULL,
      5.  `cust_city` char(50) DEFAULT NULL,
      6.  `cust_state` char(5) DEFAULT NULL,
      7.  `cust_zip` char(10) DEFAULT NULL,
      8.  `cust_country` char(50) DEFAULT NULL,
      9.  `cust_contact` char(50) DEFAULT NULL,
      10.  `cust_email` char(255) DEFAULT NULL,
      11. PRIMARY KEY (`cust_id`),
      12.  ) ENGINE=InnoDB AUTO_INCREMENT=10006 DEFAULT CHARSET=utf8
    2. 执行语句。
      1. mysql> select * from customers where cust_zip = '44444' limit 0,1 \G;
    3. 执行计划。
      1. mysql> explain select * from customers where cust_zip = '44444' limit 0,1 \G;
      1. id: 1
      2. select_type: SIMPLE
      3. table: customers
      4. type: ALL
      5. possible_keys: NULL
      6. key: NULL
      7. key_len: NULL
      8. ref: NULL
      9. rows: 505560
      10.  Extra: Using where

      执行计划看到type为ALL,是全表扫描,每次执行需要扫描505560行数据,这是非常消耗性能的,那么下面将介绍优化方式。

    4. 添加索引。
      1. mysql> alter table customers add index idx_cus(cust_zip);
    5. 执行计划。
      1. mysql> explain select * from customers where cust_zip = '44444' limit 0,1 \G;
      1. id: 1
      2. select_type: SIMPLE
      3. table: customers
      4. type: ref
      5. possible_keys: idx_cus
      6. key: idx_cus
      7. key_len: 31
      8. ref: const
      9. rows: 4555
      10.  Extra: Using index condition

      执行计划看到type为ref,基于索引的等值查询,或者表间等值连接。

    无索引案例二

    1. 表结构同上案例相同,执行语句。
      1. mysql> select cust_id,cust_name,cust_zip from customers where cust_zip = '42222'order by cust_zip,cust_name;
    2. 执行计划。
      1. mysql> explain select cust_id,cust_name,cust_zip from customers where cust_zip = '42222'order by cust_zip,cust_name\G;
      1. id: 1
      2. select_type: SIMPLE
      3. table: customers
      4. type: ALL
      5. possible_keys: NULL
      6. key: NULL
      7. key_len: NULL
      8. ref: NULL
      9. rows: 505560
      10.  Extra: Using filesort
    3. 添加索引。
      1. mysql> alter table customers add index idx_cu_zip_name(cust_zip,cust_name);
    1. 执行计划。
      1. mysql> explain select cust_id,cust_name,cust_zip from customers where cust_zip = '42222'order by cust_zip,cust_name\G;
      1. id: 1
      2. select_type: SIMPLE
      3. table: customers
      4. type: ref
      5. possible_keys: idx_cu_zip_name
      6. key: idx_cu_zip_name
      7. key_len: 31
      8. ref: const
      9. rows: 4555
      10.  Extra: Using where; Using index

      order by使用字段,而且字段应该是索引字段。

    隐式转换案例

    隐式转换案例一

    1. mysql> explain select * from customers where cust_zip = 44444 limit 0,1 \G;
    1. id: 1
    2. select_type: SIMPLE
    3. table: customers
    4. type: ALL
    5. possible_keys: idx_cus
    6. key: NULL
    7. key_len: NULL
    8. ref: NULL
    9. rows: 505560
    10.  Extra: Using where
    1. mysql> show warnings;
    2. Warning Cannot use range access on index 'idx_cus' due to type or collation conversion on field 'cust_zip'

    上述案例中由于表结构定义cust_zip字段是字符串数据类型,而应用传入的是数字,导致了隐式转换,无法使用索引。

    解决方案:

    1. 将cust_zip字段修改为数字数据类型。
    2. 将应用中传入的字符类型改为数据类型。

    隐式转换案例二

    1. 查看表结构。
      1. mysql> show create table customers1;
      1. CREATE TABLE `customers1` (
      2.  `cust_id` varchar(10) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL,
      3.  `cust_name` char(50) NOT NULL,
      4. KEY `idx_cu_id` (`cust_id`)
      5.  ) ENGINE=InnoDB DEFAULT CHARSET=utf8
      6. mysql> show create table customers2;
      7. CREATE TABLE `customers2` (
      8.  `cust_id` varchar(10) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
      9.  `cust_name` char(50) NOT NULL,
      10. KEY `idx_cu_id` (`cust_id`)
      11.  ) ENGINE=InnoDB DEFAULT CHARSET=utf8
    2. 执行语句。
      1. mysql> select customers1.* from customers2 left join customers1 on customers1.cust_id=customers2.cust_id where customers2.cust_id='x';
    3. 执行计划。
      1. mysql> explain select customers1.* from customers2 left join customers1 on customers1.cust_id=customers2.cust_id where customers2.cust_id='x'\G;
      1.  *************************** 1. row ***************************
      2. id: 1
      3. select_type: SIMPLE
      4. table: customers2
      5. type: ref
      6. possible_keys: idx_cu_id
      7. key: idx_cu_id
      8. key_len: 33
      9. ref: const
      10. rows: 1
      11.  Extra: Using where; Using index
      1.  *************************** 2. row ***************************
      2. id: 1
      3. select_type: SIMPLE
      4. table: customers1
      5. type: ALL
      6. possible_keys: NULL
      7. key: NULL
      8. key_len: NULL
      9. ref: NULL
      10. rows: 1
      11.  Extra: Using where; Using join buffer (Block Nested Loop)
    4. 修改COLLATE。
      1. mysql> alter table customers1 modify column cust_id varchar(10) COLLATE utf8_bin ;
    5. 执行计划。
      1. mysql> explain select cust_id,cust_name,cust_zip from customers where cust_zip = '42222'order by cust_zip,cust_name\G;
      1. id: 1
      2. select_type: SIMPLE
      3. table: customers2
      4. type: ref
      5. possible_keys: idx_cu_id
      6. key: idx_cu_id
      7. key_len: 33
      8. ref: const
      9. rows: 1
      10.  Extra: Using where; Using index
      1. id: 1
      2. select_type: SIMPLE
      3. table: customers1
      4. type: ref
      5. possible_keys: idx_cu_id
      6. key: idx_cu_id
      7. key_len: 33
      8. ref: const
      9. rows: 1
      10.  Extra: Using where

      字段的COLLATE一致后执行计划使用到了索引,所以一定要注意表字段的collate属性的定义保持一致。

    总结

    在使用索引时,我们可以通过explain查看SQL的执行计划,判断是否使用了索引以及发生了隐式转换,创建合适的索引。索引太复杂,创建需谨慎。

  • 数据库变慢的分析

    问题描述:用户的数据库发现相同的一条sql 语句,数据量百万级左右,在原来SQL 中执行大概是0.015s,而在云数据库下直接运行是5分左右,执行非常的慢,已经严重的影响了用户使用云数据库使用的信心。

    可能原因:为什么在用户的数据库上执行只需要0.015s,而到云数据库后变为了5分?根据经验,很有可能是SQL 的执行计划改变了,而导致执行时间剧增。

    问题排查:通过explain 查看sql 的执行计划,一步一步进行优化。

    通过分析,我们可以从执行计划上分析b 表做了一个全表扫描(执行计划的最后一行),查看b 表中tid 并无索引,所以我们这里可以进行优化,来减少查询过程中关联的行数,来达到优化:

    ———————————————————————————————————————

     

    我们可以看到执行计划中的rows 已经从452变为了2(执行计划的最后一行),

    由于mysql 表关联只有nest loop join 这种算法,所以我们可以估算一下这里的优化:

    原始执行一:1055789*1*1*1*1*452 扫描的行数

    新执行计划二:1055789*1*1*1*1*2 扫描的行数

    执行时间:

     

    我们看到执行时间已经由原来的6分20秒下降到了10秒,我们继续优化;

    可以看到该sql 的结果集只有区区的8行,但是扫描的行数却是非常之大的(1055789*1*1*1*1*2),在优化sql 的非常关键的一点就是优化sql 的执行路程,t=s/v;如果我们能够优化S,那么速度肯定会一下子提上来; 那么我们在看看sql 中最后的一句:

    -> WHERE EXISTS

    -> (SELECT 1 FROM xxxx_test5 b WHERE a.tid = b.tid);

    sql 查询中是要查询出每笔订单的详细信息而不得不关联其他一些表,但是最后的一个exist 限定了我们最后结果的范围,在看看xxxx_test5 这张表有多大:

    mysql> SELECT COUNT(*) FROM xxxx_test5;

    +———-+

    | COUNT(*) |

    +———-+

    | 403 |

    +———-+

    1 ROW IN SET (0.00 sec)

    mysql> SELECT COUNT(*) FROM xxxx_test5 b ,xxxx_test a WHERE

    a.tid = b.tid ;

    +———-+

    | COUNT(*) |

    +———-+

    | 8 |

    +———-+

    1 ROW IN SET (0.42 sec)

    两张表关联后只有8行记录,如果我们将订单表xxxx_test 和限定表先做关联,在和其他的一些订单信息表做连接,将会极大减小关联的行数;在进一步改写sql:

     

    分析执行计划,我们发现限定表xxxx_test5做了驱动表,驱动表的变化才是导致问题的最根本原因,扫描的行数:452*1*1*1*1; 这个时候sql 的执行速度就飞一般感觉了:

    Mysql–>;

    SELECT a.oi………….

    ……..省去结果

    8 ROWS IN SET (0.13 sec)

    总结:由于环境迁移,导致sql 执行计划改变,这就是云数据库变慢的最终原因了。

     

    案例二:隐式转换导致全表扫描

    问题描述:用户网站打开缓慢,质疑云数据库性能不好。

    可能原因:用户的数据存放在数据库中,网站访问数据库的时间较长,大多web应用程序设计,SQL没有优化或索引建立的不好导致;

    问题排查:通过查看数据库的慢日志,发现大量的慢sql,执行时间超过了2S。

    UPDATE USER SET xx=xx+N.N WHERE

    account=130000870343 LIMIT 10

    SELECT * FROM USER WHERE

    account=13056870 LIMIT 10

    怀疑在user 表上是否建立索引:

    CREATE TABLE `user` (

    `id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,

    `account` char(11) NOT NULL COMMENT ‘???’,

    …………………….

    …………………….

    PRIMARY KEY (`id`),

    UNIQUE KEY `username` (`account`),

    …………………….

    ) ENGINE=InnoDB CHARSET=utf8 ;

    查看执行计划,居然查询使用了全表扫描: db@3027 16:55:06>explain

    select * from user where account=13056870343;

    +—-+————-+——–+——+—————+——+———+——+——+————-+

    | id | select_type | table | type | possible_keys | key | key_len | ref |

    rows | Extra |

    +—-+————-+——–+——+—————+——+———+——+——+————-+

    | 1 | SIMPLE | t_user | ALL | username | NULL | NULL | NULL | 799 |

    Using where |

    +—-+————-+——–+——+—————+——+———+——+——+————-+

    1 row in set (0.00 sec)

    为什么这里会是全表扫描?account 上不是已经建立索引来吗?仔细一看,

    account 定义为了字符串,而传入的条件为数字,我们知道数字的精度是比字符串高的,所以这里做了隐士转换:to_number(account)=13056870343

    (to_number 为将字符串转换为数字),这样即使account 上有索引,也没法使用了,因此我们将传入的数字改为字符串:

    db@3027 16:55:13>EXPLAIN SELECT * FROM USER WHERE

    account=’13056870343′;

    +—-+————-+——–+——-+—————+———-+——

    | id | select_type | TABLE | TYPE | possible_keys | KEY |

    key_len | REF | ROWS | Extra |

    +—-+————-+——–+——-+—————+———-+——

    | 1 | SIMPLE | t_user | const | username | username | 33

    | const | 1 | |

    +—-+————-+——–+——-+—————+———-+——

    1 ROW IN SET (0.00 sec)

    可以看到数据已经能够索引到索引username 了。

    总结:由于用户在设计表结构的时候字段定义使用了字符串,而传入的条件却传入了数字造成了隐士转换,这是数据库应用中经常出现的典型问题; 数据库足够稳定,但不论在怎么强的数据库,也经不起劣质SQL 的挑战,优化sql 是长期的一项优化措施。

    从上面的三个案例,我们可以总结一下,用户在使用数据库的时候,发现数据库执行sql 超时,性能较差,连接超时等等这些问题,大多数情况下,是由于应用程序的设计,sql 没有优化,或者索引建立的不好而导致;除非实例不可用(主机down 掉,实例服务停掉,实例由于空间太大而被锁定)而导致用户应用不可用(实例的故障RDS 会有监控报警)。

Copyright © 2014-2025 奋奋的愤愤 | 京ICP备14029030号-1