概述

ES是一个搜索引擎,我们之所以要使用它就是为了借助它快速构建全文索引,帮助我们快速检索数据。

本文接着上篇文章ElasticSearch1初步使用继续来通过blogs索引实例说明如何简单的借助ES实现轻量级搜索功能。

问题

本文主要以应用ES为基本问题,主要探索ES通过GET方法进行搜索的使用方法。在实验的示例中本文也会简要的描述ES相关的理论知识。

如何使用索引进行搜索,对搜索结果进行分页,并使用简单的条件来过滤搜过结果是本文需要探讨的问题。

方法

本文采用对照RDBMS中SELECT功能的方法来描述ES中的轻量级搜索的概念,从总体上来讲,在ES中搜索数据其实和在RDBMS中SELECT数据是一样的,都可以指定搜索(查询)条件,都可以设置返回字段,也都可以进行一定的聚合运算。只不过ES搜索引擎使用全文搜索得到的结果会根据内容与搜索关键字的匹配程度给与每个结果一个权重,这个权重就作为搜索结果排序的依据。而RDBMS完全是依照ORDER BY子句中指定的排序规则进行排序的。

说明:

ES REST API所提供的完全在URL中描述参数的接口就是轻量级搜索接口,换句话说轻量级搜索接口都是使用GET方法的。所以文中示例使用的地址和API语法总结如没有对HTTP请求方法做出特殊声明,均使用GET方法。

对本文中语法说明特殊字符的声明:

  • {a} 代表a变量,如{host}代表主机名或ip地址,{index}代表索引名
  • [] 代表可出现也可不出现,一般情况下带有[]标识的变量出现和不出现会有不同的含义。对于URL开头的[http[s]://]表达式,如果省略整个表达式则系统会默认使用http协议。
  • … 对返回结果或请求内容进行部分省略。

全量数据搜索

语法

请求信息:

[http[s]://]{host}:{port}/[{index}/[{type}/]]_search

响应信息:

{
    "took": 7,//执行搜索请求的耗时,单位毫秒
    "timed_out": false,//搜索是否超时
    "_shards":{//参与本次搜索的分片信息
        "total": 5,//参与本次搜索的分票总个数
        "successful": 5,//有多少个分片成功的完成了搜索任务
        "failed": 0 //有多少分片执行搜索任务失败
    },
    "hits":{ //搜过结果信息
        "total": 1,//匹配到的文档总数
        "max_score": 1.0193838,//查询所匹配文档的 _score 的最大值。
        "hits":[...]//匹配到的文档详细信息结果集
    }
}

在没有指定分页条件的情况下,响应信息默认返回10条结果。

示例

搜索blogs索引下,articles类型下的全部文档。这是一个没有任何过滤条件的最简单的搜索。

请求信息:

http://ubuntu:9200/blogs/articles/_search

响应信息:

{
  ...,
    "hits" : [
      {
        "_index" : "blogs",
        "_type" : "articles",
        "_id" : "AV6GlxJvP5Roqj_P-AOw",
        "_score" : 1.0,
        "_source" : {
          "title" : "ES自动生成id索引",
          "author" : "为为",
          "since" : "2017-09-16 20:20:20",
          "categorie" : "搜索引擎",
          "tags" : [
            "java",
            "研发"
          ],
          "body" : "如果你无法手动对文档进行编号,可以使用POST方法向ES中索引一个新文档,其操作方法和链接规则如图8所示。"
        }
      },
      ...//更多内容暂时省略
    ]
  }
}

简单条件筛选

简单的条件筛选就像是给SQL中的SELECT语句加上WHERE子句,从而限定只查找满足某些条件的结果。

语法

在请求路径中使用q参数,并将查询条件赋值给q

[http[s]://]{host}:{port}/[{index}/[{type}/]]_search?q={param_name}:{param_value}
示例

我们搜索一下文章的tags包含 标签1 的文章。

请求参数:

http://ubuntu:9200/blogs/articles/_search?q=tags:标签1

响应参数:

{
  ...,
  "hits" : {
    ...
    "hits" : [
      {
        "_index" : "blogs",
        "_type" : "articles",
        "_id" : "1",
        "_score" : 1.0193838,
        "_source" : {
          "title" : "第一篇文章",
          "author" : "马华",
          "since" : "2017-09-10 20:20:20",
          "categorie" : "科学读物",
          "tags" : [
            "标签1",
            "标签2"
          ],
          "body" : "测试文章内容"
        }
      }
    ]
  }
}

分页

我们在SQL中SELECT语句可以使用LIMIT关键字进行分页,来保证我们每次查询只拿符合需求的数据条数。刚刚也提到过ES也支持分页,默认每页有10条数据。

语法

在搜索URL中可以使用size参数指定页大小,from应跳过的结果集条数。

请求信息:

[http[s]://]{host}:{port}/[{index}/[{type}/]]_search[?[size={size}][[&]from={from}]]
示例

不使用任何过滤条件搜索blogs索引type类型下的所有文档,指定页大小为2,从第跳过1条结果。

请求信息:

http://ubuntu:9200/blogs/articles/_search?size=2&from=1

响应信息请自行演示。

多索引和类型

如果你需要在一个或多个特殊的索引并且在一个或者多个特殊的类型中进行搜索。我们可以通过在URL中指定特殊的索引和类型达到这种效果,下面举例说明如何使用多索引或多类型。
在所有的索引中搜索所有的类型

http://ubuntu:9200/_search

在 blogs 索引中搜索所有的类型

http://ubuntu:9200/blogs/_search

在 blogs 和 pictures 索引中搜索所有的文档

http://ubuntu:9200/blogs,pictures/_search

在任何以 b 或者 p 开头的索引中搜索所有的类型

http://ubuntu:9200/b*,g*/_search

在 blogs 索引中搜索 aiticles 类型

http://ubuntu:9200/blogs/articles/_search

在 blogs 和 pictures 索引中搜索 articles 和 a_images 类型的文档

http://ubuntu:9200/blogs,pictures/articles,a_images/_search

在所有的索引中搜索 articles 和 a_images 类型的文档

http://ubuntu:9200/_all/articles,a_images/_search

当在单一的索引下进行搜索的时候,Elasticsearch 转发请求到索引的每个分片中,可以是主分片也可以是副本分片,然后从每个分片中收集结果。多索引搜索恰好也是用相同的方式工作的,只是会涉及到更多的分片。

多个搜索条件

刚刚介绍的条件搜索只能使用一个搜索条件,而我们一般的业务都需要更为复杂的搜索条件。

_all字段

在 blogs/aiticles 中搜索“为为”的相关信息,注意该搜索中并未指定“为为”属于哪个字段。

http://ubuntu:9200/blogs/_search?q=为为
同时搜索多个字段

在 blogs/aiticles中搜索author包含”为为”, title包含”ES”的信息。

http://ubuntu:9200/blogs/articles/_search?q=+title:ES自动生成id索引 +author:为为
  • 前缀表示必须与查询条件匹配。类似地, -前缀表示一定不与查询条件匹配。没有 + 或者 -的所有其他条件都是可选的——匹配的越多,文档就越相关。在存在多个条件时,如果没有明确使用default_operator=AND指定多个条件的关系为AND,则多个条件的关系为OR。
同一个字段下多种可能性

在 blogs/aiticles中搜索author包含”为为”,tags为”java”或”编码”的信息。

http://ubuntu:9200/blogs/articles/_search?q=+tags:(java 研发)  +author:为为

在 blogs/aiticles中搜索author包含”为为”,tags为”java”或”编码”的信息。

http://ubuntu:9200/blogs/articles/_search?q=+tags:(java 研发)  +author:为为&default_operator=AND

总结

经过本文示例我们可以看出ES不仅可以作为一个NoSQL数据库,存储格式化的JSON数据,其更强大的功能在于搜索。ES不仅会存储文档,文档中的每个字段都将被索引并且可以被查询 。不仅如此,在简单查询时,Elasticsearch 可以使用 所有(all)索引字段,快度返回结果,我们甚至不必指定具体要搜索哪个字段就。

总之ES的搜索可以完成以下任务:

  • 在结构化的数据(JSON)中使用结构化查询。
  • 全文检索。

    轻量级搜索虽然简单方便,但其也有缺点:

  • 当查询字符串中很小的语法错误,像 - , : , / 或者 “ 不匹配等,将会返回错误而不是搜索结果。
  • 允许任何用户在索引的任意字段上执行可能较慢且重量级的查询,这可能会暴露隐私信息,甚至将集群拖垮。

    基于以上两点原因,不推荐向用户直接开放轻量级搜索功能,一般情况下只在开发调试中使用。

引用

本文是我在学习使用ES时的笔记,在本文的写过过程中参考了大量其它资料,有些材料来源于网络,我由衷的表示感谢,但由于原作者不明,恕不能一一记述。

  1. Elasticsearch 权威指南.——https://www.elastic.co/
  2. Elasticsearch技术解析与实战/朱林编著.——北京:机械工业出版社,2016.12(数据分析与决策技术丛书)

关于

本项目和文档中所用的内容仅供学习和研究之用,转载或引用时请指明出处。如果你对文档有疑问或问题,请在项目中给我留言或发email到
weiwei02@vip.qq.com 我的github:
https://github.com/weiwei02/ 我相信技术能够改变世界 。

链接

评论和共享

elastic search初步使用

ElasticSearch是一个基于Lucene的搜索引擎,是当前世界上最受欢迎的全文搜索引擎,其主要特点如下:

  • 横向可拓展性: 往集群中增加机器时只需要更改一点配置就可以将新机器加入集群
  • 分片机制: 同一个索引切分成不同的分片
  • 高可用: 提供复制集机制,一个分片可以设置多个复制集,某台机器如果宕机不至于使集群无法工作
  • 使用简单,基于 REST api就可以完成搜索引擎的全部工作,所需学习成本低。

如无特殊声明,本文和后续文档将 Elastic Search 简称为ES

全文搜索

全文搜索是指搜索程序扫描整个文档,通过一定的分词方法,对每一个词建立索引,并指明该词在文档中出现的位置和次数,最后搜索引擎再通过索引关键字搜索出文档并返回给用户的过程。

Lucene

Lucene是Apache下的一个开源全文搜索引擎工具,提供了完整的查询引擎和索引引擎和部分文本分析引擎。不过Lucene仅仅是一个工具包,其目的是为了让研发人员能够通过这些工具包快速的为自己的应用搭建一个搜索引擎或者基于这些工具包开发出一个完整的搜索引擎。

倒排序索引

Lucene中的索引采用的是倒排序索引的模式。所谓的倒排序索引(Inverted Index)是通过属性的值来确定整条纪录的位置,而不是由纪录来确定属性的值。倒排索引把普通索引中的文档编号和值的关系倒过来,变成:“关键词”对“拥有该关键词的所有文章号”。带有倒排索引的文件我们称为倒排索引文件,简称倒排文件(inverted file)。倒排索引主要就由索引关键字和倒排文件所组成。

建立搜索引擎的关键步骤就在于建立倒排索引,倒排索引一般以以下数据结构出现:

关键字 文章号[出现频率] 出现位置
简单 1[3] 2,9,89
美女 5[1] 1
tip 9[2] 2,7,22

表1 倒排索引数据结构示例

倒排索引在存储上使用LSM树,维护其索引方法是当需要新增文档进入系统时,首先解析文档,之后更新内存中维护的临时索引,文档中出现的每个单词,在其倒排表列表末尾追加倒排表列表项;一旦临时索引将指定内存消耗光,即进行一次索引合并,这里需要倒排文件里的倒排列表存放顺序已经按照索引单词字典顺序由低到高排序,这样直接顺序扫描合并即可。

实现

lucene在实现倒排索引时将索引划分为词典文件(Term Dictionary)、频率文件(Frequencies)、位置文件(Positions)保存。其中词典文件不仅包含关键词,还有该关键词指向频率文件和位置文件的指针。

为了节省存储空间,提升索引效率。Lucene还对索引进行了压缩。

安装

ES使用JAVA语言开发,所以在安装ES之前需要在系统中安装有JDK。

表2是本文中的示例所使用的软件环境信息。

类型 名称 版本
操作系统 ubuntu server 16.04.2 LTS
内核 Kernel 4.4.0-62-generic
容器 docker 1.28
java openjdk 1.8.0_141
搜索引擎 elasticsearch 5.5.2

表2 本机软件环境信息

docker镜像安装

为了方便部署,我直接采用docker镜像的方式搭建ES。镜像启用命令为:

$ docker run -d -v "$PWD/esdata":/usr/share/elasticsearch/data --name elasticsearch -H elasticsearch elasticsearch  -Etransport.host=0.0.0.0 -Ediscovery.zen.minimum_master_nodes=1 elasticsearch

系统内核参数

由于elastic search需要用到nio和mmap(虚拟内存映射)技术,在启动该镜像之前首先需要检查一下系统内核对虚拟内存映射数目的限制(vm.max_map_count参数)是否大于262144,如果不满足这个条件,elastic search镜像将不会启动。

我们可以通过以下两种方式去设置系统内核的vm.max_map_count参数:

sysctl -w vm.max_map_count=262144

如果想永久修改此参数,可以通过修改 /etc/sysctl.conf 文件的方式使其永久性的成为系统内核参数。

测试

在linux系统下,我们可以使用 curl 程序来完成REST接口的调用与测试,《Elasticsearch权威指南》对 curl 调用REST接口的方式描述如图1所示。

image
图一 curl命令示意图

使用以下命令可以检测elastic search是否启动成功。

curl -i -XGET 'localhost:9200/'

-i 参数是说明要打印http请求头信息,GET是请求方法,localhost:9200/就是我们服务器的地址和端口,elastic search默认与外部交互的端口就是9200.如果服务启动成功,你会收到类似虾苗的响应信息。

HTTP/1.1 200 OK
content-type: application/json; charset=UTF-8
content-length: 331

{
  "name" : "Franz Kafka",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "JP677C9kRNqjEWYnFae_gQ",
  "version" : {
    "number" : "5.5.2",
    "build_hash" : "b2f0c09",
    "build_date" : "2017-08-14T12:33:14.154Z",
    "build_snapshot" : false,
    "lucene_version" : "6.6.0"
  },
  "tagline" : "You Know, for Search"
}

使用

为了方便操作,本文与后续内容都将使用chrome浏览器的 Restlet Client插件来模拟REST请求,在这里推荐一下,感谢该插件的作者将本插件开源。

创建索引

Elastic Search就像是一个nosql数据库一样,存储我们要进行查询的信息,在ES中,数据库名就是索引名。一个 Elastic Search 集群可以包含多个索引,相应的每个索引可以包含多个类型 。这些不同的类型存储着多个文档 ,每个文档又有多个属性。

我们可以使用PUT方法创建blogs库,详细请求信息如图2所示。

image
图二 创建blogs数据库

如果收到以下回应内容,则说明blogs数据库创建成功。

image
图3 创建blogs数据库成功的回应信息

使用GET地址 http://ubuntu:9200/_cat/indices?v可以查看库的状态,返回结果如下:

health status index uuid                   pri rep docs.count docs.deleted store.size pri.store.size
yellow open   blogs Y9hkRePSQmiAzjNQ_K_FSw   5   1          0            0       810b           810b

其中yellow代表健康度,状态是活动,索引为blogs,主分片数量5,复制集1,已存储的文档数为0.

新建文档

ES是一个面向文档的数据库,每一个文档都代表一条完整的实体记录,本文实例中一个文档就代表一篇文章。存储一个文档到ES的行为就叫做索引,在索引一个文档之前,首先应该明确应该将文档存储在哪里。

本文对于数据存储使用以下设计:

  • 每一篇文章被定义为一个文档,包含作者,发布时间,文章分类,正文等信息。
  • 每个文档都属于articles类型
  • articles类型保存在blogs索引里
  • blogs索引在我们的ES集群中。

    按照这个设计思路,我们向ES中索引一篇文章,其请求信息图4所示。

image
图4 向blogs索引articles类型插入一篇新文档

如果新建文档成功,则会收到HTTP状态码为201的回应,回应信息如图5所示。

image
图5 向blogs索引articles类型插入一篇新文档回应信息

接着我们再索引几篇新文档,如图6,图7.
image
图6 向blogs索引articles类型插入更多新文档
image
图7 向blogs索引articles类型插入更多新文档。

如果你无法手动对文档进行编号,可以使用POST方法向ES中索引一个新文档,其操作方法和链接规则如图8所示。
image
图8 向blogs索引articles类型POST插入一篇新文档

从其相应结果中可以看出ES通过自己的规则为文档增加了_id字段,相应结果如图9所示.
image
图9 向blogs索引articles类型POST插入一篇新文档响应信息

检索文档

在ES中检索已存在的文档只需使用GET方法访问刚刚新增文档时的连接就可以了。

GET http://ubuntu:9200/blogs/articles/2

返回结果如图10所示。
image
图10 从blogs索引articles类型检索文档

_index属性代表文档所属索引, _type是文档所属类型, _id是文档编号, _version是文档的版本(每对文档做一次修改, _version就会加1), _found代表是否查询到指定文档, _source就是文档中所存储的内容。

更新与删除文档

使用REST API对ES中的数据进行修改或删除也是极其方便的。如果我们要删除某个文档,只需使用DELETE方法访问这个文档的链接就可以了。

DELETE http://ubuntu:9200/blogs/articles/2

我们先介绍一种全量更新文档的方法,使用PUT方法访问我们的文档地址,参数信息加上文档的新内容就可以全量更新文档了。文档更新后,ES会犯规给我们新的文档版本和操作结果。

示例,对文档2进行全量更新,其请求与回应信息如图11,图12所示。

image
图11 PUT文档全量更新
image
图12 PUT文档全量更新响应

ES的文档不能被修改,只能被替换。但ES为我们提供了 update API。通过update API操作从整体来看,我们可以对文档的某个位置进行部分更新。不过在底层实现上 update API 还是需要进行完整的检索-修改-重建索引 的处理过程。 区别在于这个过程发生在分片内部,这样就避免了多次请求的网络开销。因为减少了检索和重建索引步骤之间的时间,更新此文档时与其他进程的更新操作冲突的可能性也会减少。
update 请求最简单的一种形式将待更新的字段作为doc的参数, 它只是与现有的文档进行合并。对象被合并到一起,覆盖现有的字段或者新增字段。

示例,修改文档2的author和tags属性,其请求、新文档内容如图13、图14所示。
image
图13 POST update文档部分更新
image
图14 POST update文档部分更新后的结果

如果更新文档是一个并发操作,当前工作在更新文档的检索步骤会获取当前版本号,在重建索引之前会检查文档此时的版本号是否与检索时的版本号一致,如果版本号不一致,当前线程就会发生操作失败,放弃文档的修改。如果业务上允许重复更新的话,可以通过retry_on_conflict属性,来设置失败重试。该属性用法如下面代码所示。对于不能重试的业务场景,update API提供了version属性进行乐观并发控制。

PUT http://ubuntu:9200/blogs/articles/2/_update?retry_on_conflict=5

搜索文档

搜索是ES的核心,不过鉴于篇幅原因,关于搜索功能的使用将放到下篇文章中。

引用

本文是我在学习使用ES时的笔记,在本文的写过过程中参考了大量其它资料,有些材料来源于网络,我由衷的表示感谢,但由于原作者不明,恕不能一一记述。

  1. Elasticsearch 权威指南.——https://www.elastic.co/
  2. Elasticsearch技术解析与实战/朱林编著.——北京:机械工业出版社,2016.12(数据分析与决策技术丛书)

    非原创声明

    本文并非我的原创文章,而是我学习jvm时的笔记。文中的材料与数据大部分来自于其它资料,详细请查看本文的引用章节。

关于

本项目和文档中所用的内容仅供学习和研究之用,转载或引用时请指明出处。如果你对文档有疑问或问题,请在项目中给我留言或发email到
weiwei02@vip.qq.com 我的github:
https://github.com/weiwei02/ 我相信技术能够改变世界 。

链接

评论和共享

概述

言之有物,论之有据才是有效的交谈。

我们是工程师,我们的理想是做出完美的产品,我们希望人类能够通过我们的产品来改变某种生活习惯。在很多情况下我们都在默默的付出,为商务人员对客户的售后提供强力而有效的技术保障。可能是因为我们习惯了默默的奉献,平时没有注意过与人交流,故而会出现自己明明知道的东西却说不出来的情况。我们是曾经努力过,我们是做出许多傲人的成绩,但是如果我们不能说出来,别人会知道吗?难道能让别人去猜我们是什么样的人?

默默的付出其实就是在闭门造车,我们要把自己的想法的打算说出来,我们要告诉产品经理我们队建设良好应用的看法。我们要告诉领导,我们对架构的设计。我们要告诉其它工程师,我们对代码的想法。或许你真的是才华横溢,但如果连介绍自己都不会,没人会看好你。我们是工程师,不是程序员也不是码农,我们需要为自己的闭门造车打开一扇窗户。

本文主要是以我亲身的经历,从发言者的角度来探索该如何去发言,如何表达出内心的想法。本文也是我对自己犯过的错的反思。

问题

我们勤奋努力,我们有理想有追求,我们认为做比说更重要,但是我们无法完美的表达自己,我们的辛苦几乎不被人理解。

层次

层次就是对具体事物的抽象,想要有效的表达你的思想。不要再试图向业务人员/客户或与项目不相干的人员介绍你的代码,他们只关心功能。我曾经试过向客户从技术角度来解释我所负责的项目的优势,我告诉他以我们的的系统经过预编译,多级缓存等策略优化后,我们的系统能够在双机环境下吞吐量达到300。客户只回复了我:”xx公司的系统并发能达到5000。”听见客户这么说我真的想给他解释清楚在双机条件下涉及到大数据分析的时候,我们无法使用内存存储全部数据,所谓的并发5000真的是不可能,xx公司肯定是在骗人。

工程师都知道,系统访问一次数据库都需要几十至数百毫秒,何况我们业务的要求每次核心操作都需要操作数十次数据库中的数据。先不说应用进程与数据库进程之间的通信损耗,单是磁盘寻址时间就是现代服务器无法克服的性能瓶颈。但是我们能告诉客户吗?即便花上一两个小时给客户分析明白为什么说300就是极限,为什么5000是骗人又有什么意义呢?客户关心的只是业务,只要能在保证数据精准无差的前提下把功能给实现了,这就是合格。

一个项目有技术层次的理解,有业务层次的理解,有对细节方面的理解,也有对整体流程的理解。我们要给项目进行一个抽象,分别建立出业务层,架构层,代码层和核心功能层,简要介绍层。当遇到一个圈外人只想知道这个项目能干嘛的时候,我们只需要给别人简单的说一下简要介绍层的知识。例如当一个从没用过微信的人问你微信可以干嘛,你就应该回答他:“微信是一个实时聊天工具,可以免费的跟朋友进行聊天,通话与视频。”可以试想一下如果你拿业务层的知识来回答:“微信可以发表情,表情是一种比聊天更好玩的交流方式。可以建群聊,可以发朋友圈,朋友圈是。。。”,这样对方一定很疑惑。如果你直接回答:“微信是一个C/S架构的,有数万台服务集群提供服务,拥有2亿用户高性能高可用的应用。。。”对方一定会直接懵了。

在向别人介绍你你自己或你的产品之前,首先你要对要介绍的内容根据听众的需求进行抽象,抽象的方法就是尽量讲些让听众能够清晰理解的,尽量少用专业名词。当然恰当的使用专业名词更能说明你的专业性。

条理

条理不清晰是一个贬义词,意思是说一个人说话天马行空,无迹可寻。条理不清晰就意为着可能你滔滔不绝的长篇大论过后,听众都不知道你在说什么。

以你工作中都是如何进行性能优化这个话题为例。你可能不假思索的就可以回答:

  • 首先我会优化慢SQL,因为在现在系统中一般情况下性能瓶颈都是出现在数据库上,所以第一步先优化SQL。
  • 代码上的优化,如不要在循环中写异常捕获等具体优化方法。
  • 看系统中有没有长时间阻塞的线程,优先进行去锁优化,如果实在需要保证线程安全,那就尽量使用轻量级锁,读写锁,注意重入锁等作为优化策略。
  • 其次根据实际需要设置大小合适的堆空间,然后选择合适的垃圾收集算法(如注重响应时间就选择CMS或G1)。

    上述回答的确是一般的优化方法,你能回答以上的答案可以证明你确实做过性能优化,知道一般的优化点在哪里,但是这么讲解着实有不妥之处。当我们要回答一个问题时,我们首先需要知道在哪会发生这个问题,然后寻找问题发生的原因,最后再提出解决方案,动手解决问题。性能优化和我们在生活中遇见的大多数问题一样,没有什么固定程序的解决方案。

    还回到上个问题上面,当我们发布性能优化的论述时,如果按照以下步骤可能有条理点:

  • 当项目不再能满足性能需求时,首先我们需要找到性能的瓶颈点。可以通过linux的top命令分析一下各个服务器的主要瓶颈是在cpu上、内存上还是I/O上。

  • 首先是应用服务器,如果瓶颈在CPU上,首先定位最耗资源的进程,借助jvm的一些工具如JProfiles,jvisualvm等工具我们还可以将问题定位到代码上。然后再详细分析哪些操作造成了高密度的CPU运算,尝试是否有优化的余地。

  • 如果系统CPU阻塞时间比很大且I/O负载过高,则首先去分析这个负载是来自于磁盘还是网卡,则尝试寻找降低阻塞原因,找到硬件资源瓶颈,然后使用NIO等技术降低系统阻塞。

  • 如果系统内存占用很大,则需要定位具体进程。如果占用内存过大的是我们的应用,则需要查看JVM堆空间设置,看是否是堆空间设置不合理或者使用了大量的直接内存而没有释放。

  • 如果还没定位到问题,则尝试分析JVM中的线程快照,看是否有死锁或长时间持有互斥锁的线程,并尝试优化这些。

  • 接着才是垃圾回收策略的等虚拟机参数级的优化。

  • 若问题在以上应用服务器优化思路下没有优化空间,或者优化后也无法满足性能需求,可以考虑布置集群或给集群增加机器。

    以上几条只是简要的说明了应用服务器的优化思路,对于数据库服务器和缓存服务器,根据他们不同的业务需要有专属的优化方案,本文不再对此作出详细讨论。当我们要向听众去介绍一个方案或设计时,可以依照先定位问题,再分析问题,最后再提出解决方案,实施方案的过程来逐步的展现我们的思想。在这种条理下我们的私立也鞥更加清晰,不至于因漫谈而忘记重点。

总结

刚刚说到了条理,我们怎么能让我们演讲时更有条理呢?想要有条理就要善于总结,并且将总结作为一种习惯。只有经过总结后的语言才能更有条理,除非你有很高的演讲技巧,否则不要将总结的时间放到你将要说话前一刻,不然你在演讲前的紧张会让你根本无心总结。

为演示总结的好处,我在这里提问一个简单的问题,什么样的代码才是好代码?

作为工程师,你一定有自己的衡量代码好坏的标准,不知你是否总结过这个问题? 假如你没有总结过,这个问题你可能这么回答:

  • 可扩展性
  • 易读性
  • 架构,符合项目需求
  • 代码不要太冗余
  • 整体的风格保持统一
  • 好性能

    当然你可能有回答出其他的特性,我们暂且抛开,我这边有几条经过总结后可能更好的答案:

  • 领域驱动,完全满足需求,可测试

  • 整洁规范,注释清晰,可读性高
  • 简单高效,逻辑清晰,性能优秀
  • 善于抽象,层次合理,避免重复
  • 高内聚,低耦合,易于扩展维护

事实

事实胜于雄辩!再好的理论如果没有实例的支撑也只是空谈。

比如我们上面所说到的“总结”这一节里,我们总结了几条好代码的标准,乍一听好像很有理,但仔细想又空无一物,这种情况出现的原因就是我们没有拿出事实论证,没有实例去支撑。对于“简单高效,逻辑清晰,性能优秀”这条,我们可以进行一个举例说明。假如我们以前使用100行代码才能完成一个Excel文件导出的代码,而现在我们只需要10行代码就能完成这项工作,那么这10行代码相对来说就是好代码。

对于其它几条所涉及的事实,这边不再一一讲述,如果你对此有兴趣,可以翻看一下spring的源码,看看spring是如何的设计成“高内聚,低耦合,易于扩展维护”如何层次合理。

故而当我们需要表达时,说出理论总结后,或者在总结前,尽量的拿出具体事实来为自己的总结做一个证据,让别人对你的观点深信不疑。

最后

在文章的背后我还是要说明,我们是工程师,应当以知识为重。所有的交流都在我们具有业务知识或技术知识的前提之下,所以不可因锻炼交流放弃知识学习。你会什么很重要,把你的知识或者知识转化的产品分享出来同样重要。

引用

在写作本文的受过一位阿里的不知名的前辈的提点,为为在此谢过。今天必是我在成为架构师之路上迈出的又一关键步伐。

关于

本项目和文档中所用的内容仅供学习和研究之用,转载或引用时请指明出处。如果你对文档有疑问或问题,请在项目中给我留言或发email到
weiwei02@vip.qq.com 我的github:
https://github.com/weiwei02/ 我相信技术能够改变世界 。

评论和共享

摘要

工欲善其事,必先利其器。作为JAVA的开发者,如果说会使用jdk是工作的基础,那么了解jdk的实现原理,甚至自己写一些框架或包就是工作的进阶。尤记得听一名老工程师跟我说过,等你把jdk都研究透了,你就是一名合格的高级工程师。Map集合在JAVA开发中很重要,就常用程度来讲,其在现代业务开发的代码里出现的频率位居前列。今天这篇文章主旨就是探索Map及其常见的实现类。
本文着重从jdk8代码出发来分析Map,与之响应的会讨论部分数据结构和算法方面的知识。我希望读者你已经有了很好的java基础,能够了解常用的设计模式并对面向对象编程思想拥有深刻的理解。本文会忽略部分过于基础的内容和不在本文讨论范围内的部分API。
首先,文章会简要的介绍Map接口,接着去分析jdk中几个最常见的Map接口的实现类。文章会花些笔墨在HashMap这个最常见的Map类上,其次会去分析TreeMap等类。最后文章还会对比HashMap来研究适合在并发场景下使用的ConcurrentHashMap,来分析它究竟是如何优化才能保证HashMap的线程安全性的。

方法

基本接口

Map是一种键-值对应的数据结构,java.util.Map 接口定义了Map常用的api。接下来我以代码的形式简要的对Map接口进行介绍。

package java.util;

import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.io.Serializable;

/**
 * 返回map中键值对集合的数目
 */
int size();

/**
 * 判断map中是否有键值对信息,如果该Map为空则返回 true 。
 */
boolean isEmpty();

/**
 判断map中是否有指定的key元素,如果有该元素,则返回 true 。
 */
boolean containsKey(Object key);

/**
 如果map中有一个或多个key所映射的值为value,则返回 true 。
 */
boolean containsValue(Object value);

/**
 根据指定的key在map中获取与之对应的value并返回。
 */
V get(Object key);

/**
 将key/value对放入到map中,如果map中已经存在了键为key的元素,则将原来的值替换为新value。
 */
V put(K key, V value);

/**
 从map中移除键为key的元素并返回
 */
V remove(Object key);


/**
将目标集合中的所有元素全部放到该map中
 */
void putAll(Map<? extends K, ? extends V> m);

/**
 清空集合中的所有元素
 */
void clear();


/**
 返回map中所有key的集合
 该方法所返回的集合并非创建一个全新集合,如果对该集合进行删除操作会直接影响到原来map中的元素
 */
Set<K> keySet();

/**
 返回map中所有value的集合
 该方法所返回的集合也与map有对应关系,对map本身所做的删除操作会进而影响到此集合
 */
Collection<V> values();

/**
 返回map中的元素集合
 该元素集合与map还存在映射关系
 */
Set<Map.Entry<K, V>> entrySet();

/**
 * map元素接口
 将map元素封装成键-值对的格式
 */
interface Entry<K,V> {
  ...//Entry中的内容暂不介绍
}

boolean equals(Object o);

int hashCode();

// Defaultable methods

/**
 返回指定的key所对应的value或默认值
 * @since 1.8
 */
default V getOrDefault(Object key, V defaultValue) {
    V v;
    return (((v = get(key)) != null) || containsKey(key))
        ? v
        : defaultValue;
}

/**
 对map集合进行遍历操作
 * @since 1.8
 */
default void forEach(BiConsumer<? super K, ? super V> action) {
    Objects.requireNonNull(action);
    for (Map.Entry<K, V> entry : entrySet()) {
        K k;
        V v;
        try {
            k = entry.getKey();
            v = entry.getValue();
        } catch(IllegalStateException ise) {
            // this usually means the entry is no longer in the map.
            throw new ConcurrentModificationException(ise);
        }
        action.accept(k, v);
    }
}

/**
 将符合条件的map中的元素进行替换
 * @since 1.8
 */
default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
    Objects.requireNonNull(function);
    for (Map.Entry<K, V> entry : entrySet()) {
        K k;
        V v;
        try {
            k = entry.getKey();
            v = entry.getValue();
        } catch(IllegalStateException ise) {
            // this usually means the entry is no longer in the map.
            throw new ConcurrentModificationException(ise);
        }

        // ise thrown from function is not a cme.
        v = function.apply(k, v);

        try {
            entry.setValue(v);
        } catch(IllegalStateException ise) {
            // this usually means the entry is no longer in the map.
            throw new ConcurrentModificationException(ise);
        }
    }
}

/**
 如果map中没有指定的key,或指定的key对应的value值为null,则将key-value中的value放入到map中,并返回新value。如果map中已经存在该key,且value不是null,则返回原值。
 * @since 1.8
 */
default V putIfAbsent(K key, V value) {
    V v = get(key);
    if (v == null) {
        v = put(key, value);
    }

    return v;
}

/**
以同时指定键值对的方式来从map中移除元素,返回操作成功状态
 * @since 1.8
 */
default boolean remove(Object key, Object value) {
    Object curValue = get(key);
    if (!Objects.equals(curValue, value) ||
        (curValue == null && !containsKey(key))) {
        return false;
    }
    remove(key);
    return true;
}

/**
 如果原元素存在,则将原元素的值替换为新值
 * @since 1.8
 */
default boolean replace(K key, V oldValue, V newValue) {
    Object curValue = get(key);
    if (!Objects.equals(curValue, oldValue) ||
        (curValue == null && !containsKey(key))) {
        return false;
    }
    put(key, newValue);
    return true;
}

/**
 当前的key在map中有对应的映射时,使用新元素替换原有元素
 * @since 1.8
 */
default V replace(K key, V value) {
    V curValue;
    if (((curValue = get(key)) != null) || containsKey(key)) {
        curValue = put(key, value);
    }
    return curValue;
}

/**
 如果指定的key在map中的映射为空(或者map中不包含此key),则尝试使用给定的算法去计算一个值作为key的映射,并将这个元素存放进map中。
 * @since 1.8
 */
default V computeIfAbsent(K key,
        Function<? super K, ? extends V> mappingFunction) {
    Objects.requireNonNull(mappingFunction);
    V v;
    if ((v = get(key)) == null) {
        V newValue;
        if ((newValue = mappingFunction.apply(key)) != null) {
            put(key, newValue);
            return newValue;
        }
    }

    return v;
}

/**
 如果map中已有指定的key和其映射,则尝试使用给定的函数重新计算一个value替换原值
 * @since 1.8
 */
default V computeIfPresent(K key,
        BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
    Objects.requireNonNull(remappingFunction);
    V oldValue;
    if ((oldValue = get(key)) != null) {
        V newValue = remappingFunction.apply(key, oldValue);
        if (newValue != null) {
            put(key, newValue);
            return newValue;
        } else {
            remove(key);
            return null;
        }
    } else {
        return null;
    }
}

/**
 使用给定的函数计算一个在map中与key对应的新值用来替换掉原来与key映射的value
 * @since 1.8
 */
default V compute(K key,
        BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
    Objects.requireNonNull(remappingFunction);
    V oldValue = get(key);

    V newValue = remappingFunction.apply(key, oldValue);
    if (newValue == null) {
        // delete mapping
        if (oldValue != null || containsKey(key)) {
            // something to remove
            remove(key);
            return null;
        } else {
            // nothing to do. Leave things as they were.
            return null;
        }
    } else {
        // add or replace old mapping
        put(key, newValue);
        return newValue;
    }
}

/**
 如果在map中与key所对应的value为null,则使用新value去替换原来的值,否则就使用remappingFunction在原值和新值之间计算出一个值用来做key的映射。
 * @since 1.8
 */
default V merge(K key, V value,
        BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
    Objects.requireNonNull(remappingFunction);
    Objects.requireNonNull(value);
    V oldValue = get(key);
    V newValue = (oldValue == null) ? value :
               remappingFunction.apply(oldValue, value);
    if(newValue == null) {
        remove(key);
    } else {
        put(key, newValue);
    }
    return newValue;
}
}

/*

*/

我们看到,jdk8对map类进行了大量的改动,增加了很多带有默认方法的api,其中包括如foreach等实用的api。

Map骨架

java.util.AbstractMap 类是一个抽象类,其对map接口提供了最简单的模版方法实现,如求map的entrySet(),hashCode()等方法。我们常用的类HashMap就是继承自此类,不过由于此类实现的时间较早(jdk1.2),当前大部分继承自此类的的类激斗都对此类的方法进行了更符合性能的优化重写。

哈希表 (HashTable)

java.util.HashTable是数据结构中的哈希表的java实现,其内部存储key-value形式的键值对元素,任何不是null的对象都可以被当做key或value。为了能够正常的检索、存储对象,所有的key都应该有可用的hashCode()方法。哈希表的性能主要由初始化大小和负载因数两个重要的要素所影响,HashTable基于大量数据进行测验与分析,综合考虑存储时的空间兼用与检索时的时间消耗情况比,将负载因数的初始值设置为 0.75f ,如无必要情况,无需修改此值。对于初始化大小这个参数,其默认值为11,我们可以根据实际业务任意进行定制。HashTable是线程安全的,但是在实际开发使用中在没有线程安全的场景下java官方推荐我们直接使用HashTable,在高并发的场景下,我们更应该优先考虑使用java.util.concurrent.ConcurrentHashMap类。

public class Hashtable<K,V>
  extends Dictionary<K,V>
  implements Map<K,V>, Cloneable, java.io.Serializable {

  /** 哈希表数据实际存放在Entry数组中 */
  private transient Entry<?,?>[] table;

  /** 哈希表中entry的数量 */
  private transient int count;

  /* 重新进行hash计算的阈值 */
  private int threshold;

  /** 哈希表的负载因数 */
  private float loadFactor;

  /**
   哈希表已修改数据结构的次数
   增加或减少Entry节点的数目或对哈希表进行扩容都会触发该值的变化,modCount主要用来在高并发场景下进行快速失败使用。
   */
  private transient int modCount = 0;

  /** 序列化版本id */
  private static final long serialVersionUID = 1421746759512286392L;


}
/**
while(i.hasBext()){
  Entry e = i.next();
  if(value == e.getValue || value.equals(e.getValue))
    return true;
  }
  return false;
  ****/

哈希表的基本属性中有一个 threshold 属性,这个属性代表着重新分配哈希空间的阈值,当哈希表中存放的数据超出此值所标识的大小,哈希表就会进行自动扩容并重新计算 threshold 值,其计算公式为 threshold = (int)(capacity * loadFactor)

首先我们分析一下用来检查是否包含某元素的contains()方法:

public synchronized boolean contains(Object value) {
      if (value == null) {
          throw new NullPointerException();
      }

      Entry<?,?> tab[] = table;
      for (int i = tab.length ; i-- > 0 ;) {
          for (Entry<?,?> e = tab[i] ; e != null ; e = e.next) {
              if (e.value.equals(value)) {
                  return true;
              }
          }
      }
      return false;
  }
  public synchronized V get(Object key) {
      Entry<?,?> tab[] = table;
      int hash = key.hashCode();
      int index = (hash & 0x7FFFFFFF) % tab.length;
      for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
          if ((e.hash == hash) && e.key.equals(key)) {
              return (V)e.value;
          }
      }
      return null;
  }

  /** 为哈希表添加节点 ****/

  private void addEntry(int hash, K key, V value, int index) {
      modCount++;

      Entry<?,?> tab[] = table;
      if (count >= threshold) {
          // Rehash the table if the threshold is exceeded
          rehash();

          tab = table;
          hash = key.hashCode();
          index = (hash & 0x7FFFFFFF) % tab.length;
      }

      // Creates the new entry.
      @SuppressWarnings("unchecked")
      Entry<K,V> e = (Entry<K,V>) tab[index];
      tab[index] = new Entry<>(hash, key, value, e);
      count++;
  }

  /** 向哈希表中存放某值 */
  public synchronized V put(K key, V value) {
      // Make sure the value is not null
      if (value == null) {
          throw new NullPointerException();
      }

      // 确认哈希表中不存在该key
      Entry<?,?> tab[] = table;
      int hash = key.hashCode();
      int index = (hash & 0x7FFFFFFF) % tab.length;
      @SuppressWarnings("unchecked")
      Entry<K,V> entry = (Entry<K,V>)tab[index];
      for(; entry != null ; entry = entry.next) {
          if ((entry.hash == hash) && entry.key.equals(key)) {
              V old = entry.value;
              entry.value = value;
              return old;
          }
      }

      addEntry(hash, key, value, index);
      return null;
  }
  /** 向哈希表中添加节点 ****/
  public synchronized void clear() {
      Entry<?,?> tab[] = table;
      modCount++;
      for (int index = tab.length; --index >= 0; )
          tab[index] = null;
      count = 0;
  }

  public Set<K> keySet() {
      if (keySet == null)
          keySet = Collections.synchronizedSet(new KeySet(), this);
      return keySet;
  }

HashTable的contains方法只能遍历查找,其时间复杂度是O(n),在最坏的情况下我们可能需要遍历整个哈希表中的所有Entry。HashTable中可能发生线程安全问题的方法都使用 synchronized 关键字添加互斥锁,如此以来,虽然保证了其线程安全性,但对方法实现互斥会让整个对象都受影响,这也间接的给出了java不推荐在高并发的场景下使用HashTable的解释。其get()方法首先对key进行哈希运算然后通过取模等操作计算出key所在的索引,然后进行遍历操作,其时间复杂度为 O(logn)。这种算法被称之为哈希查找,拥有极高的效率,比较推崇。向哈希表中存放数据时,首先根据key的hash计算索引,然后在索引的table上找到其Entry链表,遍历该链表中是否已存在该key,如果已存在则将value替换,否则就在table上index索引的Entry链表后追加该元素。这种算法原理符合人类的思维形式,比较利于理解,可计算机CPU并不擅长求余计算,进行哈希查找时有一定的计算资源浪费。至于clear()方法就是遍历清除table上的每一个节点,将哈希表的大小置0。等待垃圾回收时直接就将哈希表中的内容全部回收,释放内存。哈希表的keySet()等方法也都是线程安全的,其实现原理就是通过Collections.synchronizedSet()方法将原有的keySet结合哈希表本身作为互斥锁转化成线程安全的集合。keySet的迭代操作使用哈希表内部进行专有优化的迭代器进行迭代,在保证线程安全的前提下,尽量加快迭代速度。

HashMap 经典容器

HashMap是我们在开发中最为钟爱的Map类,使用时简单方便,且性能极高。其内部实现由自己定制化的高效hash算法,jdk8更是对HashMap做出了很多的优化。在使用时HashMap允许null作为其键或值,增加了API的灵活性,但是HashMap并不是线程安全的,请注意不要在并发使用此类。HashMap的内部基本存储方式是大致可以看成是哈希表,当HashMap中的某个哈希桶内部的链表存储的元素达到一定数目时,随着链表的不断拓展,查找元素的算法的时间复杂度将上升到O(n)。为了能够保证HashMap的效率,当链表的程度大于阈值(默认为8)时,这个哈希桶就会被转化成效率更高红黑树。

HashMap的属性代码:

评论和共享

总览

所有的弱点中,最大的弱点就是害怕暴露弱点。

注重实效的程序员对他自己的职业生涯负责,并且不害怕承认无知或错误。

负责

责任是你主动承担的东西。面对责任除了尽你所能之外,还需要分析风险是否已经超出了你的控制。对于不可能或风险太大的事,有权不为之负责。当需要承担责任时,你必须依照自己的道德标准和判断来做出决定。

控制

假如在团队开发中,出现了一个无法控制的事务,且没有完美的解决,该事件久了能引发更多不可预知且不可控的事件。不可控的事件的熵总会趋向于最大化,这就是即便有着详细的计划,技术高超的开发者,但是软件还会开发失败的原因。项目成员的心理对项目有很大的作用,如果这种不可控变多了,最终软件可能因此腐烂。

标准

人们总会以最低标准来要求自己,特别是在有参照物的时候,人会更倾向于拿自己和最低的那个参照物相比较。在软件开发中,如果有一点很低劣的代码,或者管理者的一个错误决策,都有可能引起一场雪崩反应。人们总会容易产生这样的想法:“相中中有那么烂的代码,我只要写那样的代码就可以了,没必要太严格的要求自己。”,至于说项目在这之前有多好,代码在这之前结构有多清晰,注释有多明确,谁会去关注呢?

成就

首先,你有对整个系统的设计,整体构思。但是你不需要把整个系统全部拿出来,把自己的要求和期望全部详细的讲出来。在现今的企业管理架构中,同时请求太多资源或者对团队成员提太多任务,可能会遇到拖延和茫然。并且开会研讨,讨论审批,会让事情变得复杂化。每个人都会护卫他们自己的资源,不要让他们觉得你是哪个掠夺者。
可以先设计一部分比较合理的东西,等开发完成之后,就拿出来给大家看,首先先享受部分成功的喜悦,然后引导式的提出更多的功能需求或更多的优化点,最后等其它项目成员来确定并承担下这些任务。就这样,逐渐的把它们引导向整个项目的图景。
对于人们来说,参与正在发生成功的事会更容易让他们接受。能够让他们瞥见未来,你就能把它们聚集在周围。

大局

大多数软件腐烂都是从一点点的微不足道的小事开始的,大多数的项目拖延都是一天一天发生的。系统一个特征一个特性的偏离其规范,一个又一个的补丁被打到某段代码上,知道最初的代码一点也没留下,并且项目的设计也已经变得杂乱无章。常常是小事的积累破坏了士气和团队。
留心大图景,要观察项目的全局设计,要观察周围的环境和发生的事,而不只是做你自己在做的事情。

质量

我们无法精确的去控制软件的质量。作为研发者我们必须在保证代码的整洁高效的前提下,去满足软件用户的需求。
不管是公共库还是应用软件,我们的作品始终是要给用户使用的。质量需求很重要,尽量给用户直接参与权衡质量需求的空间。你应该将系统的范围和质量作为系统需求的一部分,规定下来。
无视用户的需求,一味的给程序增加新特性,或是一次又一次的润饰代码,等等行为都不是有职业素养的做法。
我们决不能向用户或其它领导许诺一个不可能兑换的时间标度,到最后为了赶上项目的最后期限而削减基本的工程内容。

收手

如果不懂止步,所有的辛苦劳作都会被破坏。
不要因为过度修饰和过于求精而毁坏完好的程序。让你的代码凭自身的质量运行一段时间,他也许不完美,你不用因此而担心它,没有完美的代码。

知识

知识上的投资总能得到更好的回报,你的知识和经验是你最重要的职业财富。但是这些财富是时效资产,随着新技术,语言和环境的出现,你的知识会变得过时。随着你的知识价值降低,对你的公司和客户来说你的价值也在降低。

知识资产

软件工程师所知道的关于计算技术和他们所工作的领域的全部事实、以及他们所有的经验都可以被视作他们的知识资产(Knowledge Portfolios)。管理知识资产与管理金融资产非常相似。

  1. 严肃的投资者会将定期投资作为一种习惯。
  2. 多元化是长期成功的关键。
  3. 聪明的投资者在保守的投资和高风险、高回报的投资之间平衡他们的财产。
  4. 投资者设法低买高卖,以获取最大回报。
  5. 应周期性的重新评估和平衡你的财产。
    要想在职业生涯中获得成功,你必须用同样的指导方针管理你的知识资产。最总要的是你需要定期为你的知识资产投资。

学习方法

这里给出一些学习技术的方法,以供参考。

  • 每年至少学习一种新语言。
    不同的语言以不同的方式解决相同的问题。通过学习若干种不同的方法,可以帮助你拓宽思维,并避免墨守成规。
  • 每季度阅读一本技术书籍。
  • 也要阅读一些非技术书籍。
    记住计算机是由人来使用的,你所做的软件也是为了满足人的需求。不要忘记等式这边的人,这很重要。
  • 上课。
  • 参加本地用户组织。
  • 实验不同的环境。
  • 跟上潮流。
  • 上网。
    是否在某个项目中使用这些技术,或者是否把它们放入你的简历,这些都不重要。重要的是学习的过程将拓宽你的思维,使你向着新的可能性和新的做事方式拓展。

批判的思考

批判的思考你听到的和读到的,你要警惕你所得到的知识的准确性,保持自己的大脑不要被错误的言论或知识所误导。

交流

最重要的问题不是你有什么,还要看你怎样去包装它。就算你拥有最好的主意、最漂亮的代码、或是最注重实效的想法,如果没有有效的交流,最终也会毫无结果。

主旨

在交流之前,首先规划好你要交流的东西,写出一个大纲。以能够表达出自己想要说的内容为目标,不断的提炼他。如果有时间的话,应该先去准备好几种把你要说的内容讲清楚的策略。

听众

只有当你是在传达信息时,你才是交流。为此,你需要了解你的听众的需要、兴趣和能力。我们不想看到这种情况:一个研发人员发表长篇独白,讲解各种神秘的技术优点,把所有的听众都弄得目瞪口呆。在我17年8月去宁波某银行演示系统的时候就犯过这种错误,后来回顾总结的时候意识到这种行为不是交流,而是空谈。下面引入一首离合诗,或许能够帮助你进行交流:

What do you want them to learn? 你想让他们学到什么?
What their interest in what you have got say? 他们对你讲的什么感兴趣?
How sophisticated are they? 他们有多丰富的经验?
How much detail do they want? 他们想要多少细节?
Whom do you want to own the information? 你想让谁拥有这些信息?
How can you motivate them to listen to you? 你如何促使他们听你说话?

时机

说话的时机很重要,不要因为分不清事情的轻重缓急,弄错了说话的时机导致无法达到本来的目的。

风格

根据不同的听众调整你的交流风格,有些人喜欢正式的“事实”简报,有些人喜欢在进入正题之前首先高谈阔论一番。如果需要正式的文档的话,在注重事实的基础上,尽量让你的文档美观一些。如果可能的话,在文档制作早期就让你的用户参与到文档的制作当中。获取他们的反馈,汲取他们的智慧。对于风格而言,请谨记:你怎么说和你说什么同样重要。

做倾听者

如果你想让别人听你讲话,首先你要学会听别人讲话。在进行交流的过程中,要学会及时的回复他人。除非你生活在真空中,你才不需要交流,否则请好好锻炼一下交流的技巧。交流的越有效,你的影响力就越大。

引用

本文是对软件工程师为人哲学的一点总结,主要参考了以下资料:

  1. 《马维达. 程序员修炼之道[J]. 程序员, 2004(4):121-124.》
非原创声明
>本文并非我的原创文章,而是我读书笔记。文中的材料与数据大部分来自于其它资料,详细请查看本文的引用章节。

关于

本项目和文档中所用的内容仅供学习和研究之用,转载或引用时请指明出处。如果你对文档有疑问或问题,请在项目中给我留言或发email到
weiwei02@vip.qq.com 我的github:
https://github.com/weiwei02/ 我相信技术能够改变世界 。

评论和共享

JVM虚拟机-垃圾收集器

发布在 jvm

摘要

垃圾收集器(GC,Garbage Collector):自动化的内存管理工具,垃圾收集器主要做以下几种工作:

  • 在新生代中给新对象分配内存,并将老对象放到老年代中去管理。(Allocating objects to a young generation and promoting aged objects into an old generation.)
  • Java HotSpot VM在Java堆内存占用率超过默认阈值时触发标记阶段,标记阶段将通过并行标记的方式在老年代中寻找存活的对象。(Finding live objects in the old generation through a concurrent (parallel) marking phase. The Java HotSpot VM triggers the marking phase when the total Java heap occupancy exceeds the default threshold.)
  • 通过并行复制的算法压缩存活的对象从而恢复可用内存。(Recovering free memory by compacting live objects through parallel copying.)

    如何选择垃圾收集器才能使应用的性能达到最优?HotSpot团队都难以对这个问题给出完美的答案。一般而言,暂停应用程序进行垃圾回收的频率越低且每次垃圾回收的时间越短,应用程序的性能就会越高。但是拥有许多大数据对象的多线程高事务率的大型应用的垃圾回收很难同时满足以上两点。

    阿姆达定理(系统中对某一部件采用更快执行方式所能获得的系统性能改进程度,取决于这种执行方式被使用的频率,或所占总执行时间的比例。)说明,给定问题的并行加速度由问题的顺序执行部分限制其性能。其意为者部分工作并不能完美的并行化,只能串行的执行。在JAVA平台也受此定理的影响,JAVA SE 1.4之前并不支持并行垃圾回收,故垃圾收集器在多处理器下对性能的影响随程序的并行化而增长。

引言

小至简单的 public static void main 程序,大至拥有数百台服务器集群的大型WEB服务,全世界目前有几十亿部设备正在使用JAVA。为了支持这种多样化的部署,Java HotSpot虚拟机(Java HotSpot VM)提供了多个垃圾收集器,每个垃圾回收器被设计为满足不同的需求。JAVA应用程序可以根据自身运行的类型(调试版或生产版)与载体物理机的配置情况,自动的去选择合适的垃圾收集器。但当需求上需要我们写出更加高性能的应用程序时,JAVA的默认配置就很难再满足我们的需求。此时便需要作为开发或运维人员的你通过设置一些JVM参数,明确的指定垃圾收集器等方式,来达到性能需求上所指明的条件。

在阅读本文之前,我希望你已经读过我的上篇文章JVM虚拟机-垃圾回收算法,对常见的垃圾回收算法及其在HotSpot虚拟机下的实现有了解,如果说垃圾回收算法是理论基础,那么本文所介绍的垃圾收集器就是JVM垃圾收集的具体实现。另外希望你在工作或研究的过程中感受过JAVA默认的垃圾回收机制给你带来过性能上的压力,因为只有亲身的经历过,才能感受到了解垃圾收集器的机制的重要性。

本文会以JAVA8版本的HotSpot虚拟机为基础环境逐个的介绍每个垃圾收集器的实现原理并分析其优缺点,简略的探讨一下什么样的应用在什么样的环境下应该选用哪种垃圾收集器。本文的目标是能够帮助读者对应用程序进行JVM调优,做出高性能的应用。

本文着重对CMS和G1两款垃圾收集器进行介绍分析。

基础

HotSpot虚拟机提供了三种大类的垃圾收集器,每种垃圾收集器都有不同的特征和性能表现。简单描述如下:

  • 串行类垃圾收集器( serial collector):
    串行类垃圾收集器是最受争议的垃圾收集器,串行垃圾收集器使用单个线程完成所有的垃圾收集工作。其工作时不需要与其它的线程进行交互,所以垃圾回收的效率是很高的。串行类垃圾收集器尤其适合单CPU的硬件平台上使用,且在使用数据集小于100MB的多CPU硬件平台上也能取得很好的效果。HotSpot在Client模式下默认使用串行垃圾收集器,在其它环境下也可以通过使用 -XX:+UseSerialGC 参数来明确指定使用串行垃圾收集器。

  • 并行垃圾收集器(Parallel Collector):
    并行垃圾收集器又被称为吞吐量优先垃圾收集器(Throughput Collector),通常我们所指的吞吐量优先垃圾收集器指的都是适用于新生代的 Parallel Scavenge 垃圾收集器。并行垃圾收集器能够显著的减少多CPU环境下中型或大型应用垃圾收集所消耗的时间,HotSpot Server版本默认使用的使用的垃圾收集器就是并行垃圾收集器。通过启动虚拟机的时候指定参数 -XX:+UseParallelGC 可以手动指定年轻代使用吞吐量优先收集器。与年轻代相呼应的还有老年代并行垃圾收集器(Oracle 称其为 Parallel compaction 意为并行压缩),当使用参数 -XX:+UseParallelGC 时,JVM老年代的默认开启 Parallel compaction ,同时也可以通过参数 -XX:-UseParallelOldGC 来手动开启。

    • 并发垃圾收集器(Concurrent Collector):
      从字面上理解,并发垃圾收集器的意思是让用户线程和垃圾回收线程并发运行。其在大中型应用系统中并发垃圾收集器工作的过程中仅仅有很短的 ‘Stop the world’ ,且相对于并行垃圾收集器能够大幅度的减小系统响应时间(可能以牺牲系统吞吐量为代价)。JAVA8版本的HotSpot虚拟机下目前有两款并发收集器,CMS垃圾收集器(通过参数 XX:+UseConcMarkSweepGC 开启)和G1(通过参数 -XX:+UseG1GC 开启)。

探索

引用

本文是对JVM垃圾收集器的学习笔记,笔记的内容并非是原创,而是大量参考其它资料。在写作本文的过程中引用了以下资料,为为在此深深谢过以下资料的作者。

  1. 《The Java® Virtual Machine Specification · Java SE 8 Edition》
  2. 《深入理解Java虚拟机:JVM高级特性与最佳实践/周志明著.——2版.——北京:机械工业出版社,2013.6》
  3. 《Java Platform,Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide》

    非原创声明

    本文并非我的原创文章,而是我学习jvm时的笔记。文中的材料与数据大部分来自于其它资料,详细请查看本文的引用章节。

关于

本项目和文档中所用的内容仅供学习和研究之用,转载或引用时请指明出处。如果你对文档有疑问或问题,请在项目中给我留言或发email到
weiwei02@vip.qq.com 我的github:
https://github.com/weiwei02/ 我相信技术能够改变世界 。

评论和共享

JVM虚拟机-垃圾回收算法

发布在 java, jvm

引言

C和C++ 程序员能够直接操作内存,凭自己的需要来决定何时去申请多大内存,何时去释放这块内存。他们甚至可以使用指针,来确定的去操作某块内存地址。作为后来者的JAVA远远不如他们强大,我认识的JAVA程序员可能有半数都从未去关注过内存。在使用JAVA进行业务开发时,我们不需要去关注对象到底存储在内存的哪个位置,也不需要关注这个对象到底占用了多大的内存空间,更不需要特意的为某个对象去释放内存空间。需要我们关注的,只有如何完成产品需求,在规定时间内交付合格的产品给客户。如果你对编程的看法跟上面所述的类似,或者你很认同“完成比完美更重要”这条工程师信条,那么这篇文章就不太适合你来阅读了。

内存控制给编程界造成了一个具有魔性的圈,圈内的人想出去,圈外的人想进来。就像C程序员早就受够了内存的申请与释放的折磨,受够了各种内存错误。而很多像你一样的JAVA程序员,立志要写出高性能应用的人,一直在用心地思索怎么才能写出性能更好的多线程应用,当这个追求达到一定层次的时候,你甚至已经开始在乎JAVA垃圾回收所消耗的时间了。由于JAVA自身的限制,我们没办法进入到内存控制的圈内,但我们可以通过设置JVM参数等方式,选择适当的垃圾收集器或设置垃圾回收线程对JAVA的垃圾回收机制进行优化。

概述

本文的重点是介绍JVM虚拟机下常用的垃圾回收算法的理论知识,并不介绍具体算法实现代码。在阅读本文之前,希望读者已经掌握了JVM的内存划分设计、对象引用的可达性分析算法与JAVA的四种对象引用级别(Strong Reference/Soft Reference/Weak Reference/Phantom Reference)等相关知识。在本文之后,我还会再去整理一些关于垃圾收集器等方面的知识。文中所涉及的代码或理论都已在JDK8中进行验证。

垃圾回收算法

标记-清除算法(Mark-Sweep)

首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

标记-清除算法的工作过程如图1所示。

图1 标记-清除算法示意图

  • 优点: 算法简单清晰,其它垃圾收集算法都是据此算法的思想并对其不足改进而得到的。
  • 缺点: 1.效率低,标记和清除需要遍历内存,效率极低。2.回收内存之后产生大量的内存碎片,当内存碎片过多时,应用需要给大对象分配内存时无法分配连续内存。

复制算法(Copying)

将内存按容量划分为完全等大小的两块,每次只使用其中一块内存。当这一块内存用完后,将还活着的对象全部复制到另一块上面,然后把这一块上已使用的内存空间全部清理掉。其示意图如图二所示。


图2 复制算法示意图

  • 优点: 实现简单,运行高效。每次都是对整个半区进行内存回收,内存分配时无需考虑内存碎片等复杂情况。
  • 缺点: 浪费内存空间。在最坏的情况下,这种垃圾回收算法可能浪费了一半的内存空间。

    在JAVA应用中,90%之上的对象的生存时间都极短,所以JVM把内存分为一块较大的Eden空间和两块较小的Survivor空间,每次只使用Eden和其中一块Survior空间。当需要进行垃圾回收时,将Eden和Survivor中还处于可达状态的对象一次性的复制到另一块Survivor空间中,最后清空Eden和刚刚使用的Survivor空间。当Survivor空间不够用时,还会将长时间存活的对象转存的老年代中。在HotSpot虚拟机中,默认的Eden和Survivor空间的比例是8:1,这样可以做到只浪费10%的内存。

标记-整理算法(Mark-Compact)

标记-整理算法的标记过程与标记-清除算法的标记过程类似,但在标记完成后并不是直接对可回收的对象进行清理,而是让所有正在存活的对象都向前端移动,然后直接清理掉边界以外的内存。其示意图见图3.


图3 标记-整理算法示意图

  • 优点: 节省内存空间,提升内存运用率,且不会产生内存碎片。
  • 缺点: 性能低。

分代收集算法(Generational Collection)

根据对象的存活时间把内存分为新生代和老年代,根据个代对象的存活特点,每个代采用不同的垃圾回收算法。

分代收集就是根据不同代的特性,使用最合适的垃圾回收算法进行垃圾回收。如新生代中,每次垃圾收集都会有大量的对象死去,只有极小部分对象存活,所以更适合复制算法。老年代中对象存活率高,且没有额外的内存空间为它进行分配担保,所以更适合用标记-清除或标记-整理算法。

HotSpot算法实现

枚举根节点(GC Roots)

在垃圾回收时,我们要想办法找出哪些对象是存活的,一般会选取一些被称为GC Root的对象,从这些对象开始枚举。在进行GC Root枚举时要求所有对象停下来,也就是JVM所称的“Stop the world”。所有的算法实现都会将虚拟机停下来的,否则分析结果的准确性将无法保证。

由于HotSpot采用准确式GC,该技术主要功能就是让虚拟机可以准确的知道内存中某个位置的数据类型是什么,比如某个内存位置到底是一个整型的变量,还是对某个对象的 reference。这样在进行 GC Roots 枚举时,只需要枚举 reference 类型的即可。在能够准确地确定 Java 堆和方法区等 reference 准确位置之后,HotSpot 就能极大地缩短 GC Roots 枚举时间,所以当执行系统停顿下来之后,虚拟机不需要遍历所有的根节点和上下文去确定GC Roots,而是存在着一个OopMap的数据结构来达到这个目的。

在类加载完成的时候,虚拟机就会把什么类的什么偏移上是什么类型的数据计算出来。在JIT编译的时候也会在特定位置记下在寄存器和栈中哪些位置是引用,GC在扫描时就可直接得到信息。

安全点(Safepoint)

Safepoint:会导致 OopMap 内容变化的指令非常多,如果为每一条指令都生成对应的 OopMap,那么将需要大量的额外空间,这样对导致 GC 成本很高,所以 HotSpot 只在 “特定位置” 记录这些信息,这些位置被称为 安全点(Safepoint)。并非程序在任意时刻都可以停顿下来进行 GC,而只有程序到达 安全点(Safepoint) 以后才可以停顿下来进行 GC;所以安全点既不能太少,以至于 GC 过程等待程序到达安全点的时间过长,也不能太多,以至于 GC 过程带来的成本过高。

由于在 GC 过程中必须保证程序已执行,那么也就是说 必须等待所有线程都到达安全点上方可进行 GC。一般来说有两种解决方案可以选择:

  • 抢先式中断:不需要线程的执行代码去主动配合,当发生 GC 时,先强制中断所有线程,然后如果发现某些线程未处于安全点,那么将其唤醒,直至其到达安全点再次将其中断;这样一直等待所有线程都在安全点后开始 GC。现在几乎没有虚拟机使用这种方式。

  • 主动式中断:不强制中断线程,只是简单地设置一个中断标记,各个线程在执行时轮询这个标记,一旦发现标记被改变(出现中断标记)时,那么将运行到安全点后自己中断挂起;目前所有商用虚拟机全部采用主动式中断。

安全区(Safe Region)

安全点机制仅仅是保证了程序执行时不需要太长时间就可以进入一个安全点进行 GC 动作,但是当特殊情况时,比如线程休眠、线程阻塞等状态的情况下,显然 JVM 不可能一直等待被阻塞或休眠的线程正常唤醒执行,此时就引入了安全区的概念。

安全区(Safe Region):安全区域是指在一段代码区域内,对象引用关系等不会发生变化,在此区域内任意位置开始 GC 都是安全的。线程运行到Safe Region中的代码时,首先标记自己进入了安全区,然后在这段区域内,如果线程发生了阻塞、休眠等操作,JVM 发起 GC 时将忽略这些处于安全区的线程。当线程再次被唤醒时,首先他会检查是否完成了 GC Roots枚举(或这个GC过程),然后选择是否继续执行,否则将继续等待 GC 的完成。

引用

本文是对JVM垃圾收集算法的学习笔记,笔记的内容并非是原创,而是大量参考其它资料。在写作本文的过程中引用了以下资料,为为在此深深谢过以下资料的作者。

  1. 《The Java® Virtual Machine Specification · Java SE 8 Edition》
  2. 《深入理解Java虚拟机:JVM高级特性与最佳实践/周志明著.——2版.——北京:机械工业出版社,2013.6》
  3. 《Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide》

    非原创声明

    本文并非我的原创文章,而是我学习jvm时的笔记。文中的材料与数据大部分来自于其它资料,详细请查看本文的引用章节。

关于

本项目和文档中所用的内容仅供学习和研究之用,转载或引用时请指明出处。如果你对文档有疑问或问题,请在项目中给我留言或发email到 weiwei02@vip.qq.com
我的github: https://github.com/weiwei02/
我相信技术能够改变世界。

评论和共享

引言

网站的伸缩性是指不需要改变服务器的硬件设计,仅仅靠改变应用服务器的部署数量,就可以扩大或缩小服务器的处理能力。一般来说,网站的伸缩性设计可分为两类,一类是根据功能进行物理分离实现伸缩,一类是单一功能通过集群实现伸缩。前者是不同服务器部署不同的服务,提供不同的功能。后者是集群中多台服务器部署相同的服务,提供相同的功能。

方法

依据功能实现伸缩

从网站发展早期,通过增加服务器提高网站处理能力时,新增服务器总是从现有服务器中分离出部分功能和服务。
通过物理实现服务器伸缩.jpg
图1 通过物理实现服务器伸缩

图1所描述的伸缩性就是每次伸缩操作都会有更多的服务器加入网站,新增的服务器用于处理特定的服务。依据功能进行进行架构伸缩的具体实现方案有两种,纵向分离和横向分离。

  • 纵向分离
    纵向分离是指服务根据业务流程上的不同部分分开进行部署,实现系统上的伸缩性,其分离方式如图2所示。
    纵向分离部署实现系统可伸缩性.jpg
    图2 纵向分离部署实现系统可伸缩性

  • 横向分离
    横向分离又称为业务分割,其实现方式是将不同的业务模块分离部署,实现可伸缩性,如图3所示。根据系统的性能要求可以将横向分离的力度做到非常小。
    横向分离.jpg
    图3 横向分离部署实现系统可伸缩性

通过集群实现伸缩

将不同功能分离部署可以实现一定程度的伸缩性,但是随着网站访问量的逐步增加,即使分离到最小力度的独立部署,单一的服务器也不能满足业务规模的需求。此时可以使用服务器的集群,将相同的服务部署到多台服务器上构成一个集群整体对外提供服务。具体来说,集群伸缩性又可分为应用服务器集群伸缩性和数据服务器集群伸缩性。而数据服务集群也可分为缓存数据服务器集群和存储数据服务器集群。

应用服务器集群的伸缩性设计

应用服务器应该被设计成无状态的,即应用服务器不存储请求上下文信息,如果将部署有相同应用的服务器组成一个集群,用户的每一个请求都可以发送到任意一台服务器上去处理,任何一台服务器处理的结果都是相同的。这样只要能将用户请求按照某种规则分发到集群的不同服务器上,就可以构成一个应用服务器集群,每个用户的请求都可能落在不同的服务器上。
如果HTTP请求分发装置可以感知或者可以配置集群服务器的数量,可以及时发现集群中新上线或下线的服务器,并能向新上线的服务器发送请求,停止向已下线的服务器分发请求,那么就实现了应用服务器的伸缩性。在这里,这个HTTP请求分发装置被称为负载均衡服务器。 使用负载均衡服务器实现应用服务器可伸缩架构如图4所示。
负载均衡.jpg
图4 负载均衡实现系统可伸缩性

负载均衡是大型网站必不可少的技术手段,不但可以实现网站的可伸缩性,同时还可以改善网络的可用性。负载均衡的具体实现也多种多样,可以使用专有硬件也可以使用软件来实现。

HTTP重定向负载均衡

利用HTTP重定向协议实现负载均衡,如图5所示。
请求重定向.jpg
图5 HTTP请求重定向负载均衡原理

HTTP重定向服务器时一台普通的WEB服务器,其唯一的功能就是根据用户的HTTP请求计算一台真实的WEB服务器地址,并将该WEB服务地址写入HTTP重定向响应中(响应状态码为302)返回给用户浏览器。这种负载均衡的优点是实现比较简单。缺点是浏览器需要两次请求服务器才能进行一次访问,性能较差。其次重定向服务器自身的处理能力有可能成为瓶颈,整个集群的伸缩性规模有限。再者使用HTTP302响应状态码重定向,有可能使搜索引擎判断为SEO作弊,降低搜索引擎排名。

DNS域名解析负载均衡

这是利用DNS进行域名解析请求时进行负载均衡处理的一种方案。每次域名解析请求都会根据负载均衡算法可能计算出一个不同的IP地址,以后该用户对网站的请求都发送到这个地址上。如图6所示。
DNS域名解析.jpg
图6 DNS域名解析负载均衡原理

在DNS服务器中配置多个A纪录,如: www.mysite.com IN A 10.0.0.1、www.mysite.com IN A 10.0.0.2、www.mysite.com IN A 10.0.0.3
优点:将负载均衡的工作交给DNS,省掉了网站的管理维护负载均衡服务器的麻烦,同时许多DNS还提供了了基于地理位置的域名解析,即会将域名解析成距离用户地理位置最近的一个服务器地址,这样可加快用户的访问速度,改善性能。
缺点:目前的DNS是多级DNS,每级DNS都可能对网站的DNS信息进行缓存,当下线某台服务器后,即使修改了DNS,也需要很长时间才能生效。其次,DNS负载均衡的控制权在域名运营商那里,网站无法做更多的改善和更强大的管理。
在实际使用中,大型网站总是部分使用DNS域名解析,利用域名解析作为第一级负载均衡手段。

反向代理负载均衡

反向代理服务器在网络部署上位于WEB服务器之前,其拥有两个网卡,分别拥有内部和外部两套IP地址。在使用中它可以将外部网卡的请求转发到内部网卡连接的网络中的WEB服务器。WEB服务器的响应信息也需要反向代理服务器转发给用户。由于反向代理负载均衡工作在HTTP协议层面,因此也叫作应用层负载均衡。其逻辑架构图如图7所示。
反向代理.jpg
图7 反向代理负载均衡原理

优点:和反向代理服务器部署在一起,部署简单。
缺点:反向代理服务器是所有请求和响应的中转站,其性能可能会成为瓶颈。

IP负载均衡

IP负载均衡是在网络层修改目标IP地址来实现负载均衡,其网络架构图如图8所示。
IP负载均衡.jpg
图8 IP负载均衡原理

IP负载均衡在处理真是物理WEB服务器的响应数据包时比较复杂。WEB服务器的响应包需要先返回到负载均衡服务器,再由负载均衡服务器转发给用户。响应的转发操作主要有两种实现方式:

  1. 源地址转换(SNAT):负载均衡服务器在修改目的地址时同时修改源地址,将数据包源地址修改为自身的地址,这样WEB服务器响应会再次回到自身。
  2. 负载均衡服务器同时作为真实服务器的网关设备,这样所有的响应数据都需要先来到负载均衡服务器上,再进行转发。

    优点:在内核进程完成数据分发,较反向代理负载均衡(在应用中分发数据)有更好的处理性能。
    缺点:所有的请求和回应都需要经过负载均衡服务器,集群的最大响应和吞吐量不得不受制于负载均衡服务器网卡的最大带宽。对于下载或视频服务等需要大量数据传输的手段而言,难以满足要求。

数据链路负载均衡

数据链路负载均衡是在通信协议的数据链路层修改mac地址,来达到负载均衡的目的。负载均衡数据分发过程中不修改IP地址,只修改mac地址,通过配置真是物理服务器集群所有机器虚拟IP和负载均衡服务器IP地址一致,从而达到不修改数据包源地址和目的地址就可以进行数据分发的目的。由于实际处理请求的真是物理服务器IP和数据请求目的IP一致,不需要通过负载均衡服务器进行地址转换,可将响应数据包直接返回给用户浏览器,避免负载均衡服务器网卡带宽成为瓶颈。这种数据三角传输模式又成为直接路由方式(DR)。数据链路负载均衡网络架构如图9所示。
数据链路.jpg
图7 数据链路负载均衡原理

使用三角传输模式的数据链路层负载均衡是目前大型网站使用最广泛的一种负载均衡手段。在linux平台上最好的链路层负载均衡的开源产品是LVS(Linux Virtual Server)。

负载均衡算法

负载均衡服务器的实现可分为两个部分:

  1. 根据负载均衡算法和WEB服务器列表计算得到集群中一台WEB服务器的地址。
  2. 将请求发送到该地址所对应的WEB服务器上。

    具体的负载均衡算法通常有以下几种:

轮询(Round Robin, RR)

所有请求依次呗分发到每台应用服务器上,即每台服务器要处理的请求数目都相同,适合所有服务器硬件都相同的场景。

加权轮询(Weight Round Robin, WRR)

根据应用服务器硬件性能的情况,在轮询的基础上,按照权重将请求分发到每个服务器,性能较高的服务器处理更多的请求。

随机(Random)

请求被随机分配到各个应用服务器。随机也可以使用加权。

最少连接(Least Connections)

纪录每个服务器正在处理的连接数,将新到的请求分发到最少连接的服务器上。最少连接最符合负载均衡的定义,同样最少连接算法也能够使用加权。

源地址散列(Source Hashing)

根据请求来源的IP地址进行hash计算,得到应用服务器,这样来自同一个IP地址的请求总在同一个服务器上处理,请求的上下文信息可以存储在这台服务器上,在一个会话周期内重复使用,从而实现会话黏滞。

引用

本文是对可伸缩架构的学习笔记,笔记的内容并非是原创,而是大量参考其它资料。在写作本文的过程中引用了以下资料,为为在此深深谢过以下资料的作者。

  1. 《大型网站技术架构·核心原理与案例分析》 李智慧 2013 电子工业出版社

关于

我的github: https://github.com/weiwei02/
我相信技术能够改变世界。

评论和共享

高可用的数据

对于许多应用而言,数据是宝贵的,必须的资产。数据是整个应用的历史,是记录也有可能是配置信息,如果丢失了数据,那么对于某些应用来说结果可能就是毁灭性的,整个应用都有可能因此无法运行。
不同于高可用的应用或服务的设计方式,由于数据存储服务器上存储的数据不同,当某台服务器宕机之后,数据访问请求不能任意的切换到集群中其它数据服务器上。

CAP

CAP理论是分布式系统中对数据的管理而形成一套理论知识,CAP是设计分布式系统所必须考虑的架构问题。对于CAP本身可以解释如下:

  • Consistency(一致性): 数据一致更新,所有数据变动都是同步的
  • Availability(可用性): 好的响应性能
  • Partition tolerance(分区耐受性): 可靠性

    上面的解释可能显得太过抽象,举例来说在高可用的网站架构中,对于数据基础提出了以下的要求:

  • 分区耐受性
    保证数据可持久存储,在各种情况下都不会出现数据丢失的问题。为了实现数据的持久性,不但需要在写入的时候保证数据能够持久存储,还需要能够将数据备份一个或多个副本,存放在不同的物理设备上,防止某个存储设备发生故障时,数据不会丢失。

  • 数据一致性
    在数据有多份副本的情况下,如果网络、服务器、软件出现了故障,会导致部分副本写入失败。这就造成了多个副本之间的数据不一致,数据内容冲突。
  • 数据可用性
    多个副本分别存储于不同的物理设备的情况下,如果某个设备损坏,就需要从另一个数据存储设备上访问数据。如果这个过程不能很快完成,或者在完成的过程中需要停止终端用户访问数据,那么在切换存储设备的这段时间内,数据就是不可访问的。

    CAP原理认为,一个提供数据服务的存储系统无法同时完美的满足一致性(Consistency)、数据可用性(Availability)、分区耐受性(Partition Tolerance)这三个条件。对于三者的关系见图1.

    图1 CAP原理关系图

    在实际的大型网络应用中,数据的规模会快速扩张,因此数据架构的伸缩性(分区耐受性)必不可少。当规模变大之后,机器的数量也会增大,这时网络和服务器故障会更频繁出现,想要保证应用可用,就必须保证分布式处理系统的高可用性。所以在大型网站中,通常会选择强化分布式存储系统的可用性(A)和伸缩性(P),在某种程度上放弃一致性(C)。一般来说,数据不一致的情况通常出现在高并发写操作或者集群状态不稳(故障恢复,集群扩容…)的情况下,应用系统需要对分布式数据处理系统的数据不一致性有一定的了解并进行某种意义上的补偿工作,以避免应用出现数据不正确。

    CAP原理对于可伸缩的分布式系统设计具有重要的意义,在系统设计开发过程中,不恰当的迎合各种需求,企图打造一个完美的产品,可能会使设计进入两难之地,难以为继。
    具体来说,数据一致性又可分为以下几点:

  • 数据强一致
    各个副本中的数据总是强一致的。这种设计正确性很高,但是会在一定程度上损耗性能。
  • 数据用户一致
    应用访问数据时通过一定的纠错和校验机制,把多个数据可能不一致的副本的数据综合计算返回一个一致且确定的数据给用户。大型互联网架构一般采用这种设计,性能较好,并且数据不会出现错误。
  • 数据最终一致
    物理存储的数据不一致,用户访问得到的数据也可能不一致,但经过一段时间的自我修正(通常很短时间),数据会达到最终一致。该设计性能最高,但可能有数据错误。

    因为很难去同时满足CAP,大型网站通常会综合成本、技术、业务场景等条件,结合应用服务和其它的数据监控与纠错功能,使存储系统达到用户一致,保证用户最终访问数据的正确性。

    20170804 18:10:00 编辑到多种数据一致性

引用

本文是对class文件的学习笔记,笔记的内容并非是原创,而是大量参考其它资料。在写作本文的过程中引用了以下资料,为为在此深深谢过以下资料的作者。

  1. 《大型网站技术架构·核心原理与案例分析》 李智慧 2013 电子工业出版社

评论和共享

class文件介绍

发布在 java, jvm

非原创声明

本文并非我的原创文章,而是我学习jvm时的笔记。文中的材料与数据大部分来自于其它资料,详细请查看本文的引用章节。

*.class文件介绍

一般来讲.class文件是.java文件在编译器编译后生成的jvm能够运行的文件,*.class文件又常被称为字节码文件。java在创始之初,就提倡“一次编写,处处运行的概念”,在当今编程圈中这个概念早已不是什么特例。java通过将开发人员所编写的java代码编译成class文件,然后由jvm虚拟机在执行时将不分平台的class文件中的字节码,再翻译成机器码,交给硬件执行。java就是靠jvm虚拟机的这个设计来实现与平台无关的特性的。class文件不但与硬件平台和操作系统无关,也和具体的编程语言无关,就目前来说,如函数式编程语言scala与Groovy都可以通过自己的编译器将源代码编译成class文件,在jvm上运行。
综合来讲,class文件有以下两点特性:

  • 与硬件和操作系统平台无关
  • 与源码所使用的编程语言无关

class类文件的结构

每一个class文件都唯一对应着java类或接口枚举等定义信息,但类不一定都定义在class文件中,类可能是由类加载器动态生成的。

class文件所以被称之为字节码据我猜测可能是因为class文件以8位(1字节)为单位进行存储的二进制信息。字节码中各个数据项目严格按照顺序紧凑的排列,中间没有任何分割符。在需要存储整型或浮点型这些大于8位的数据项目时,则会使用Big-Endian的字节序进行存储,将最高位字节放在地址最低位,最低位字节放在地址最高位。

class文件格式采用表来存储数据,表中有无符号数和表两种数据类型。对于这两种数据类型说明如下:

  • 无符号数: 无符号数是基本的数据类型,可以用来表示数字、索引引用、数量值或者按照UTF-8编码构成的字符串。如u1,u2,u4,u8分别代表1,2,4,8个字节字节的无符号数。
  • : 表是由多个无符号数或者其它表作为数据项所组成的复合数据结构,表用于描述层次关系和复合的数据结构,整个class文件就是一张表。通常表以 _info 结尾。如表1就是一个class的表示例。
类型 名称 数量
u4 magic 1
u2 minor_version 1
u2 major_version 1
u2 constant_pool_count 1
cp_info constant_pool constant_pool_count - 1
u2 asscess_flags 1
u2 this_class 1
u2 super_class 1
u2 interfaces_count 1
u2 interfaces interfaces_count
u2 fields_count 1
field_info fields fields_count
u2 methods_count 1
method_info methods method_count
u2 attributes_count 1
attribute_info attributes attributes_count

表 1 class文件格式

class文件的顺序和格式必须严格实现按照上述规则,否则jvm将不能识别执行。

Magic Number 魔数

魔数是一个和文件后缀名相似的用于文件格式识别的约定,一般规定文件内容开头的前几个字节为文件的魔数。不同于文件后缀名很容易被用户以重命名的方式进行更改,文件的魔数作为识别手段可以更安全的确定文件的可用性。

class文件使用前4个字节作为魔数,来确定.class文件是否是一个能够被虚拟机识别的文件,其值是 0xCAFFEBABE 。

版本

class文件的第5-6个字节代表的可执行该class文件的目标虚拟机的最低次版本号(Minor Version),第7-8个字节是主版本号(Major Version)。java虚拟机可以运行比当前虚拟机版本号低的class文件,拒绝运行版本号不合法,或比自己版本高的class文件。JDK1.1的版本号是45,之后的每个大版本发布都把主版本号加1,如JDK1.2主版本号是46,JDK8的版本号是52。
JDK在编译java文件是可以通过 javac -target 1.6 ...命令来指定编译后的class文件可以在1.6的虚拟机版本上运行。

常量池

常量池是class文件中第一个表类型的数据项目,常量池是class文件中的资源仓库,是class文件结构中与其它项目关联最多的数据类型,也是占用class文件空间最大的数据项目之一。

class文件里紧随版本之后的数据项是常量池,由于常量池的数量是不固定的,所以在常量池的数据项之前放置的有一个u2类型的数据,代表常量池的大小。常量池大小的初始值是1,如常量池的大小的数据如果显示的是10,就代表该class文件中有9个常量。
常量池中主要存放两大类常量:Literal(字面常量)、Symbolic Reference(符号引用常量)。字面常量类似于java中常量的概念,如文本字符串、final关键字所声明的常量等。而符号引用常量则是编译中的概念,主要包括以下三种类型的常量。

  • 符号常量

    • 类和接口的全限定名(Fully Qualified Name)
    • 字段名称和描述符(Descriptor)
    • 方法的名称和描述符

    class文件中不会保存各个方法或字段在内存中的布局信息,而是在虚拟机加载class文件时进行动态的连接。虚拟机在运行class文件时从常量池中获取对应的符号引用,再在创建类或者运行时解析连接到具体的内存地址当中。
    常量池中的每一个常量都是一个表,在JDK8中有14种表结构的常量表。如表 2 所示。

类型 标识 描述 
CONSTANT_utf8_info 1  UTF-8编码的字符串
CONSTANT_Integer_info 3 整形字面量
CONSTANT_Float_info 4 浮点型字面量
CONSTANT_Long_info 长整型字面量
CONSTANT_Double_info 双精度浮点型字面量
CONSTANT_Class_info 类或接口的符号引用
CONSTANT_String_info 字符串类型字面量
CONSTANT_Fieldref_info 字段的符号引用
CONSTANT_Methodref_info 10 类中方法的符号引用
CONSTANT_InterfaceMethodref_info 11 接口中方法的符号引用
CONSTANT_NameAndType_info 12 字段或方法的符号引用
CONSTANT_MothodType_info 16 标志方法类型
CONSTANT_MethodHandle_info 15 表示方法句柄
CONSTANT_InvokeDynamic_info 18 表示一个动态方法调用点

表 2 常量池数据项目类型表

这14种常量结构表开始的第一个字节都是一个u1类型的标识位,其值就是表2中每项常量表类型所对应的标识列的值,代表当前常量属于哪种常量类型。这14种常量类型各自有自己不同的表结构,详情如表 3 所示。

常量 项目 类型 描述
CONSTANT_Utf8_info tag u1 值为1
length u2 UTF-8编码的字符串占用的字节数
bytes u1 utf-8编码的字符串
CONSTANT_Integer_info tag u1 值为3
bytes u4 按照Big-Endian存储的int值
CONSTANT_Float_info tag u1 4
bytes u4 按照Big-Endian存储的float值
CONSTANT_Long_info tag u1 5
bytes u8 按照Big-Endian存储的long值
CONSTANT_Double_info tag u1 6
bytes u8 按照Big-Endian存储的long值double值
CONSTANT_Class_info tag u1 7
index u2 指向全限定名常量项的索引
CONSTANT_String_info tag u1 8
index u2 指向字符串常量的索引
CONSTANT_Fieldref_info tag u1 9
index u2 指向声明字段的类或接口描述符CONSTANT_Class_info的索引值
index u2 指向CONSTANT_NameAndType_info的索引值
CONSTANT_Methodref_info tag u1 10
index u2 指向声明方法的类描述符CONSTANT_Class_info的索引值
index u2 指向CONSTANT_NameAndType_info的索引值
CONSTANT_InterfaceMethodref_info tag u1 11
index u2 指向声明方法的接口描述符CONSTANT_Class_info的索引值
index u2 指向CONSTANT_NameAndType_info的索引值
CONSTANT_NameAndType_info tag u1 12
index u2 指向该字段或方法名称常量的索引值
index u2 指向该字段或方法描述符常量的索引值
CONSTANT_MethodHandle_info tag u1 15
reference_kind u1 值必须1~9,它决定了方法句柄的的类型。方法句柄类型的值表示方法句柄的字节码行为
reference_index u2 对常量池的有效索引
CONSTANT_MethodType_info tag u1 16
description_index u2 对常量池中方法描述符的有效索引常量池在该处的索引必须是CONSTANT_Utf8_info的结构,表示方法的描述符。
CONSTANT_InvokeDynamic_info tag u1 18
bootstap_method_attr_index u2 对当前class文件中引导方法表的bootstap_methods[]数组的有效索引
name_and_type_index u2 对当前常量池的有效索引,常量池在此处必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述。

表 3 常量池中14中常量的结构总表

在表 3 中可以看到class文件所支持的所有的常量的结构信息,另外由于class中的类名、方法、字段都要用CONSTANT_Utf8_info型的常量来描述名称,所以CONSTANT_Utf8_info的最大长度也是java中类名、方法名或字段的最大长度65535,如果超出这个最大长度,便会无法编译。

访问标识

在class文件中位于常量池之后的是访问权限标识。class文件的访问标识是用于修饰这个class文件所代表的的类或接口本身的一些属性。访问标识是用一个u2类型的数据来代表的,如0x0001代表public,0x0020代表这是一个接口。具体的访问标识和标志值的对应关系见表4.访问标识是按位进行标识的,一个u2类型的数据有16位,目前其中的8位都被赋予了实际意义。

标识名称 标识值 意义
ACC_PUBLIC 0X0001 public的访问权限
ACC_FINAL 0x0010 如果该class文件代表一个类,那么该类是final修饰的(只能类才能拥有此标识)
ACC_SUPER 0x0020 使用invokespecial字节码指令的新语义
ACC_INTERFACE 0x0200 接口
ACC_ABSTRACT 0x0400 abstract(只有接口和抽象类有效)
ACC_SYNTHETIC 0x10000 本类不是用户代码所产生的类
ACC_ANNOTATION 0x2000 注解
ACC_ENUM 0x4000 枚举

表4 访问标识和标志值的对应关系表

类索引

类索引是一个u2类型的数据,用于确定这个类的全限定名。其索引值指向一个类型为CONSTANT_Class_info的常量。

父类索引

父类索引指向着当前类所继承的类的父类的全限定名,其类型信息与类索引相同。所有java类都有父类索引。

接口索引

java不支持多重继承,所以只有一个父类索引。但一个java类或接口可以实现多个其它接口,所以class的接口索引是一个数量并不固定的集合。
在声明接口索引之前,class文件首先会声明一个u2类型的数据代表接口索引的总数量,其后面紧跟者就是接口索引集合,如果该类没有实现任何接口,那么接口索引的总数量的值就是0。接口索引同样也指向常量池中的CONSTANT_Class_info类型的常量,使用CONSTANT_Utf8_info的字符串常量来表示接口的全限定名。

字段表集合

20170701 00:10:00 编辑到字段表集合

字段集合用于描述接口或类中所声明的全局变量。如public,static、final等词都是用来描述某个字段的修饰词。因为java中可用来修饰字段的修饰符有限,所以class文件对于字段的修饰符的表示是通过使用标识位来实现的。表5详细分析了字段表的数据结构。

类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attrobute_info attributes attributes_count

表5 字段表的数据结构

access_flags标识位中存放的是字段的修饰符,使用标志位进行标识区分。具体区分方式如表6字段访问标识表所示。

标识名称 标识值 含义
ACC_PUBLIC 0x0001 public
ACC_PRIVATE 0x0002 private
ACC_PROTECTED 0x0004 protected
ACC_STATIC 0x0008 static
ACC_FINAL 0x0010 final
ACC_VOLATILE 0x0040 volatile
ACC_TRANSIENT 0x0080 transient
ACC_SYNTHETIC 0x1000 该字段是否由编译器自动生成的
ACC_ENUM 0x4000 字段是否是enum

表6 字段访问标识对应表

name_index 和 descriptor_index 都是对常量池索引的引用,name_index引用的是字段的名称,descriptor_index引用的是字段和方法的描述符。

20170801 00:10:00 编辑到字段表集合

引用

本文是对class文件的学习笔记,笔记的内容并非是原创,而是大量参考其它资料。在写作本文的过程中引用了以下资料,为为在此深深谢过以下资料的作者。

  1. 《The Java Virtual Machine Specification》
  2. 《深入理解Java虚拟机:JVM高级特性与最佳实践/周志明著.——2版.——北京:机械工业出版社,2013.6》

关于

本项目和文档中所用的内容仅供学习和研究之用,转载或引用时请指明出处。如果你对文档有疑问或问题,请在项目中给我留言或发email到 weiwei02@vip.qq.com

from weiwei.wang 20170625

评论和共享

Copyrights © 2017 weiwei02. All Rights Reserved. github空间地址: https://weiwei02.github.io/ 国内空间地址: https://weiwei02.cording.me/
作者的图片

weiwei02

技术,改变世界


软件工程师


北京,海淀
国外主站 国内主站