月度归档:2017年05月

ELK搭建实时日志分析平台

前言

本文参照互联网上一篇文章进行实际操作, 但是在搭建elk平台过程中, 采用docker方式进行, 因此节省了部属的繁琐过程,因此把基于docker的操作过程, 穿插到原文中。

为了, 获取一个docker环境,  取决于您的操作系统, 对于linux本身大部分新版本的都支持docker了, 对于windows和mac os系统, 您需要安装一定的虚拟机, 或者docker tools(其实也是虚拟机,只是操作方便)。

这里是在windows上安装了vmware, 然后在vm中安装了centos系统, 然后在centos7中安装了docker系统

 

ELK(ElasticSearch, Logstash, Kibana)平台恰好可以同时实现日志收集、日志搜索和日志分析的功能

ELK平台介绍

在搜索ELK资料的时候,发现这篇文章比较好,于是摘抄一小段:

以下内容来自:http://baidu.blog.51cto.com/71938/1676798

日志主要包括系统日志、应用程序日志和安全日志。系统运维和开发人员可以通过日志了解服务器软硬件信息、检查配置过程中的错误及错误发生的原因。经常分析日志可以了解服务器的负荷,性能安全性,从而及时采取措施纠正错误。

通常,日志被分散的储存不同的设备上。如果你管理数十上百台服务器,你还在使用依次登录每台机器的传统方法查阅日志。这样是不是感觉很繁琐和效率低下。当务之急我们使用集中化的日志管理,例如:开源的syslog,将所有服务器上的日志收集汇总。

集中化管理日志后,日志的统计和检索又成为一件比较麻烦的事情,一般我们使用grep、awk和wc等Linux命令能实现检索和统计,但是对于要求更高的查询、排序和统计等要求和庞大的机器数量依然使用这样的方法难免有点力不从心。

开源实时日志分析ELK平台能够完美的解决我们上述的问题,ELK由ElasticSearch、Logstash和Kiabana三个开源工具组成。官方网站:https://www.elastic.co/products

  • Elasticsearch是个开源分布式搜索引擎,它的特点有:分布式,零配置,自动发现,索引自动分片,索引副本机制,restful风格接口,多数据源,自动搜索负载等。
  • Logstash是一个完全开源的工具,他可以对你的日志进行收集、过滤,并将其存储供以后使用(如,搜索)。
  • Kibana 也是一个开源和免费的工具,它Kibana可以为 Logstash 和 ElasticSearch 提供的日志分析友好的 Web 界面,可以帮助您汇总、分析和搜索重要数据日志。

画了一个ELK工作的原理图:

如图:Logstash收集AppServer产生的Log,并存放到ElasticSearch集群中,而Kibana则从ES集群中查询数据生成图表,再返回给Browser。

 

准备elk环境

首先您要搭建好您的docker环境, 然后按照下面的步骤进行操作

1. 下载elk的  镜像

执行下面的命令下载相关的docker镜像   docker pull  qnib/elk

由于网络原因, 这个镜像下载的比较慢, 建议找个合适时间下载备用。这个没办法解决, 或者自己翻墙解决吧。

 

2. 安装docker-compose程序

由于本程序需要 docker-compose 软件, 因此我们需要安装这个软件

安装文档在https://docs.docker.com/compose/install/中,

文档中,有几种安装方法,基本都是下载文件进行安装, 本文采用docker的方式安装它, 其实就是, 下载一个docker镜像, 然后在有一个配合的shell程序, 运行是shell程序, 将相关内容转发到容器里面进行运行等, 相关安装方法如下:

$ curl -L --fail https://github.com/docker/compose/releases/download/1.13.0/run.sh > /usr/local/bin/docker-compose

上面命令其实, 就是先用curl下载文件, 然后, 将下载内容从定向到一个文件中。
$ sudo chmod +x /usr/local/bin/docker-compose   然后修改下载后文件的可执行属性。
由于翻墙的原因, 直接下载可能下载不了, 因此可以先翻墙下载脚本,然后将内容从命名后, 直接拷贝到目标文件处, 然后在修改文件的属性就完成了。

3. 下载 elk的服务配置文件

打开页面https://github.com/ChristianKniep/docker-elk, 选择下载master文件

由于需要翻墙, 因此把下载好的, 放到这里(这个文件比较小就放了, 其他的太大, 没空间, 抱歉)docker-elk-master

其实我们就是需要里面的一个文件, docker-compose.yml

4. 获取本机的ip地址, 然后根据您的喜好可以选择给本机设置一个合适的名称,在hosts文件中, 这样就不需要直接使用ip地址了, 当然使用ip地址也可以。

5. 解压下载回来的zip文件, 提取docker-compose.yml文件, 并将这个文件拷贝到vm的centos中

6. 在centos中 打开文件    vi  docker-compose.yml   在文件最后添加如下配置

 

[root@localhost elk]# cat docker-compose.yml
elk:
  image: qnib/elk
  ports:
   - "9200:9200"
   - "5514:5514"
   - "55514:55514/udp"
   - "5601:5601"
   - "8080:80"
   - "8500:8500"
  environment:
  - DC_NAME=dc1
  - RUN_SERVER=true
  - BOOTSTRAP_CONSUL=true
  - COLLECT_METRICS=false
  - FORWARD_TO_LOGSTASH=false
  dns: 127.0.0.1
  hostname: elk
  volumes: 
   - /var/lib/elasticsearch
  privileged: true
  extra_hosts:
   - "myelk:192.168.128.189"
   - "otherhost:50.31.209.229"

请注意红色字体部分, 是原来文件中没有的, 是新添加家的, 这个两个 设置是在 启动docker服务时, 在容器内部的hosts文件中添加相关配置的设置, 因此请根据您的配置和ip地址灵活处理。

注意, 由于是yml文件, 空格是有含义的, 拷贝要带空格拷贝, 否则不工作啦!!

6. 运行 docker-compose up 编译并启动程序

在包括docker-compose.yml 文件的目录中, 运行  docker-compose up 命令, 启动这程序, 然后就基本安装完成了, 然后就可以参照后面的步骤慢慢调整这个系统了。

 

ELK平台搭建

 

系统环境

System: Centos release 6.7 (Final)

ElasticSearch: 2.1.0

Logstash: 2.1.1

Kibana: 4.3.0

Java: openjdk version  "1.8.0_65"

注:由于Logstash的运行依赖于Java环境, 而Logstash 1.5以上版本不低于java 1.7,因此推荐使用最新版本的Java。因为我们只需要Java的运行环境,所以可以只安装JRE,不过这里我依然使用JDK,请自行搜索安装。

ELK下载:https://www.elastic.co/downloads/

 

 

ElasticSearch

配置ElasticSearch:

以下是原文的安装方法, 由于我们本次采用了, docker的方式, 因此这些都省了, 但是作为文档保留, 以后需要手动安装,时参考。

tar -zxvf elasticsearch-2.1.0.tar.gz
cd elasticsearch-2.1.0

安装Head插件(Optional):

这个插件可以安装也可以不安装, 不影响后面演示, 只是查看es中数据等, 方便些(若是安装), 另外直接下载不方便, 还是翻墙下载, 然后拷贝到插件目录中为好。

./bin/plugin install mobz/elasticsearch-head

然后编辑ES的配置文件:

vi config/elasticsearch.yml

修改以下配置项:

以下配置项目在docker中也是配置好的, 基本不需要修改

cluster.name=es_cluster
node.name=node0
path.data=/tmp/elasticsearch/data
path.logs=/tmp/elasticsearch/logs
#当前hostname或IP,我这里是centos2
network.host=centos2
network.port=9200

其他的选项保持默认,然后启动ES:

./bin/elasticsearch

可以看到,它跟其他的节点的传输端口为9300,接受HTTP请求的端口为9200。

使用ctrl+C停止。当然,也可以使用后台进程的方式启动ES:

./bin/elasticsearch &

然后可以打开页面localhost:9200,将会看到以下内容:

返回展示了配置的cluster_name和name,以及安装的ES的版本等信息。

刚刚安装的head插件,它是一个用浏览器跟ES集群交互的插件,可以查看集群状态、集群的doc内容、执行搜索和普通的Rest请求等。现在也可以使用它打开localhost:9200/_plugin/head页面来查看ES集群状态:

可以看到,现在,ES集群中没有index,也没有type,因此这两条是空的。

 

Logstash

Logstash的功能如下:

其实它就是一个收集器而已,我们需要为它指定Input和Output(当然Input和Output可以为多个)。由于我们需要把Java代码中Log4j的日志输出到ElasticSearch中,因此这里的Input就是Log4j,而Output就是ElasticSearch。

配置Logstash:

tar -zxvf logstash-2.1.1.tar.gz
cd logstash-2.1.1

编写配置文件(名字和位置可以随意,这里我放在config目录下,取名为log4j_to_es.conf):

---------------------

两个---中间的部分是我们的操作, 其他的是原文的操作, 提供参考的

1. 首先用xshell登录centos中

2. 采用docker ps查看那个docker容器在工作

经过查看

[root@localhost elk]# docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                                                                                                                                            NAMES
78a0f3a1b8f6        qnib/elk            "/opt/qnib/bin/start_"   17 hours ago        Up 17 hours         0.0.0.0:5514->5514/tcp, 0.0.0.0:5601->5601/tcp, 0.0.0.0:8500->8500/tcp, 0.0.0.0:9200->9200/tcp, 0.0.0.0:55514->55514/udp, 0.0.0.0:8080->80/tcp   elk_elk_1

 

上面容器id是  78a0f3a1b8f6

3. 执行 docker exec -t -i  78a0f3a1b8f6 /bin/bash       进入容器内部进行操作, 其中红色部分, 请根据您的容器id进行替换

4. 进入容器内部后, 执行 ps -ef  获取全部的进程信息

[root@elk opt]# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 May30 ?        00:00:00 /bin/bash /opt/qnib/bin/start_supervisor.sh -n
root         9     1  0 May30 ?        00:03:00 /usr/bin/python /usr/bin/supervisord -n -c /etc/supervisord.conf
root        12     9  2 May30 ?        00:25:54 /usr/lib/jvm/jre-1.8.0/bin/java -Xms2g -Xmx2g -Djava.awt.headless=true -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFract
root        13     9  0 May30 ?        00:01:17 /bin/bash /opt/qnib/bin/start_logstash.sh
root        14     9  0 May30 ?        00:00:54 statsd

 

采用ps -ef  | grep logstash | grep agent

[root@elk opt]# ps -ef  | grep logstash | grep agent
root       268     9  1 May30 ?        00:16:16 /usr/lib/jvm/jre-1.8.0/bin/java -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -Djava.awt.headless=true -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -Xmx500m -Xss2048k -Djffi.boot.library.path=/opt/logstash/vendor/jruby/lib/jni -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -Djava.awt.headless=true -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -Xbootclasspath/a:/opt/logstash/vendor/jruby/lib/jruby.jar -classpath : -Djruby.home=/opt/logstash/vendor/jruby -Djruby.lib=/opt/logstash/vendor/jruby/lib -Djruby.script=jruby -Djruby.shell=/bin/sh org.jruby.Main --1.9 /opt/logstash/lib/bootstrap/environment.rb logstash/runner.rb agent -f /etc/logstash/conf.d/
root      7678  7517  1 May30 ?        00:12:33 /usr/lib/jvm/jre-1.8.0/bin/java -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -Djava.awt.headless=true -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -Xmx500m -Xss2048k -Djffi.boot.library.path=/opt/logstash/vendor/jruby/lib/jni -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -Djava.awt.headless=true -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -Xbootclasspath/a:/opt/logstash/vendor/jruby/lib/jruby.jar -classpath : -Djruby.home=/opt/logstash/vendor/jruby -Djruby.lib=/opt/logstash/vendor/jruby/lib -Djruby.script=jruby -Djruby.shell=/bin/sh org.jruby.Main --1.9 /opt/logstash/lib/bootstrap/environment.rb logstash/runner.rb agent -f /etc/logstash/conf.d/a.conf
[root@elk opt]#

如上图, 红色字体部分

---------------------

 

 

mkdir config
vi config/log4j_to_es.conf

输入以下内容:

# For detail structure of this file
# Set: https://www.elastic.co/guide/en/logstash/current/configuration-file-structure.html
input {
  # For detail config for log4j as input, 
  # See: https://www.elastic.co/guide/en/logstash/current/plugins-inputs-log4j.html
  log4j {
    mode => "server"
    host => "centos2"
    port => 4567
  }
}
filter {
  #Only matched data are send to output.
}
output {
  # For detail config for elasticsearch as output, 
  # See: https://www.elastic.co/guide/en/logstash/current/plugins-outputs-elasticsearch.html
  elasticsearch {
    action => "index"          #The operation on ES
    hosts  => "centos2:9200"   #ElasticSearch host, can be array.
    index  => "applog"         #The index to write data to.
  }
}

logstash命令只有2个参数:

因此使用agent来启动它(使用-f指定配置文件):

./bin/logstash agent -f config/log4j_to_es.conf

到这里,我们已经可以使用Logstash来收集日志并保存到ES中了,下面来看看项目代码。

 

 

Java项目

照例先看项目结构图:

pom.xml,很简单,只用到了Log4j库:

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

log4j.properties,将Log4j的日志输出到SocketAppender,因为官网是这么说的:

log4j.rootLogger=INFO,console

# for package com.demo.elk, log would be sent to socket appender.
log4j.logger.com.demo.elk=DEBUG, socket

# appender socket
log4j.appender.socket=org.apache.log4j.net.SocketAppender
log4j.appender.socket.Port=4567
log4j.appender.socket.RemoteHost=centos2
log4j.appender.socket.layout=org.apache.log4j.PatternLayout
log4j.appender.socket.layout.ConversionPattern=%d [%-5p] [%l] %m%n
log4j.appender.socket.ReconnectionDelay=10000

# appender console
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.target=System.out
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d [%-5p] [%l] %m%n

注意:这里的端口号需要跟Logstash监听的端口号一致,这里是4567。

Application.java,使用Log4j的LOGGER打印日志即可:

package com.demo.elk;

import org.apache.log4j.Logger;

public class Application {
    private static final Logger LOGGER = Logger.getLogger(Application.class);
    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 10; i++) {
            LOGGER.error("Info log [" + i + "].");
            Thread.sleep(500);
        }
    }
}

用Head插件查看ES状态和内容

运行Application.java,先看看console的输出(当然,这个输出只是为了做验证,不输出到console也可以的):

再来看看ES的head页面:

切换到Browser标签:

单击某一个文档(doc),则会展示该文档的所有信息:

可以看到,除了基础的message字段是我们的日志内容,Logstash还为我们增加了许多字段。而在https://www.elastic.co/guide/en/logstash/current/plugins-inputs-log4j.html中也明确说明了这一点:

上面使用了ES的Head插件观察了ES集群的状态和数据,但这只是个简单的用于跟ES交互的页面而已,并不能生成报表或者图表什么的,接下来使用Kibana来执行搜索并生成图表。

 

Kibana

配置Kibana:

tar -zxvf kibana-4.3.0-linux-x86.tar.gz
cd kibana-4.3.0-linux-x86
vi config/kibana.yml

修改以下几项(由于是单机版的,因此host的值也可以使用localhost来代替,这里仅仅作为演示):

server.port: 5601
server.host: “centos2”
elasticsearch.url: http://centos2:9200
kibana.index: “.kibana”

启动kibana:

./bin/kibana

用浏览器打开该地址:

为了后续使用Kibana,需要配置至少一个Index名字或者Pattern,它用于在分析时确定ES中的Index。这里我输入之前配置的Index名字applog,Kibana会自动加载该Index下doc的field,并自动选择合适的field用于图标中的时间字段:

点击Create后,可以看到左侧增加了配置的Index名字:

接下来切换到Discover标签上,注意右上角是查询的时间范围,如果没有查找到数据,那么你就可能需要调整这个时间范围了,这里我选择Today:

接下来就能看到ES中的数据了:

执行搜索看看呢:

点击右边的保存按钮,保存该查询为search_all_logs。接下来去Visualize页面,点击新建一个柱状图(Vertical Bar Chart),然后选择刚刚保存的查询search_all_logs,之后,Kibana将生成类似于下图的柱状图(只有10条日志,而且是在同一时间段的,比较丑,但足可以说明问题了:)  ):

你可以在左边设置图形的各项参数,点击Apply Changes按钮,右边的图形将被更新。同理,其他类型的图形都可以实时更新。

点击右边的保存,保存此图,命名为search_all_logs_visual。接下来切换到Dashboard页面:

单击新建按钮,选择刚刚保存的search_all_logs_visual图形,面板上将展示该图:

如果有较多数据,我们可以根据业务需求和关注点在Dashboard页面添加多个图表:柱形图,折线图,地图,饼图等等。当然,我们可以设置更新频率,让图表自动更新:

如果设置的时间间隔够短,就很趋近于实时分析了。

到这里,ELK平台部署和基本的测试已完成。

跨域资源共享 CORS介绍

CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。

它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

本文详细介绍CORS的内部机制

 

一、简介

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

二、两种请求

浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

只要同时满足以下两大条件,就属于简单请求。

(1) 请求方法是以下三种方法之一:

  • HEAD
  • GET
  • POST

(2)HTTP的头信息不超出以下几种字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三个值application/x-www-form-urlencodedmultipart/form-datatext/plain

凡是不同时满足上面两个条件,就属于非简单请求。

浏览器对这两种请求的处理,是不一样的。

三、简单请求

3.1 基本流程

对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。

下面是一个例子,浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段。


GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

上面的头信息中,Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequestonerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。

如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。


Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-开头。

(1)Access-Control-Allow-Origin

该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。

(2)Access-Control-Allow-Credentials

该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。

(3)Access-Control-Expose-Headers

该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。

3.2 withCredentials 属性

上面说到,CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。


Access-Control-Allow-Credentials: true

另一方面,开发者必须在AJAX请求中打开withCredentials属性。


var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。

但是,如果省略withCredentials设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials


xhr.withCredentials = false;

需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

四、非简单请求

4.1 预检请求

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUTDELETE,或者Content-Type字段的类型是application/json

非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

下面是一段浏览器的JavaScript脚本。


var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();

上面代码中,HTTP请求的方法是PUT,并且发送一个自定义头信息X-Custom-Header

浏览器发现,这是一个非简单请求,就自动发出一个"预检"请求,要求服务器确认可以这样请求。下面是这个"预检"请求的HTTP头信息。


OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

"预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。

除了Origin字段,"预检"请求的头信息包括两个特殊字段。

(1)Access-Control-Request-Method

该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT

(2)Access-Control-Request-Headers

该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header

4.2 预检请求的回应

服务器收到"预检"请求以后,检查了OriginAccess-Control-Request-MethodAccess-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。


HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

上面的HTTP回应中,关键的是Access-Control-Allow-Origin字段,表示http://api.bob.com可以请求数据。该字段也可以设为星号,表示同意任意跨源请求。


Access-Control-Allow-Origin: *

如果浏览器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出如下的报错信息。


XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.

服务器回应的其他CORS相关字段如下。


Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000

(1)Access-Control-Allow-Methods

该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。

(2)Access-Control-Allow-Headers

如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。

(3)Access-Control-Allow-Credentials

该字段与简单请求时的含义相同。

(4)Access-Control-Max-Age

该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。

4.3 浏览器的正常请求和回应

一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。

下面是"预检"请求之后,浏览器的正常CORS请求。


PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

上面头信息的Origin字段是浏览器自动添加的。

下面是服务器正常的回应。


Access-Control-Allow-Origin: http://api.bob.com
Content-Type: text/html; charset=utf-8

上面头信息中,Access-Control-Allow-Origin字段是每次回应都必定包含的。

五、与JSONP的比较

CORS与JSONP的使用目的相同,但是比JSONP更强大。

JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。

 

当 Web 资源请求由其它域名或端口提供的资源时,会发起跨域 HTTP 请求cross-origin HTTP request)。比如,站点 http://domain-a.com 的某 HTML 页面通过 <img> 的 src 请求 http://domain-b.com/image.jpg。网络上,很多页面从其他站点加载各类资源(包括 CSS、图片、JavaScript 脚本)。

出于安全考虑,浏览器会限制脚本中发起的跨域请求。比如,使用 XMLHttpRequest 和 Fetch 发起的 HTTP 请求必须遵循同源策略。因此,Web 应用通过 XMLHttpRequest 对象或 Fetch 仅能向同域资源发起 HTTP 请求。 为提升 Web 应用的可用性,浏览器必须支持跨域请求。 (译者注:这段描述跨域不准确,跨域并不一定是浏览器限制了发起跨站请求,也可能是跨站请求可以正常发起,但是返回结果被浏览器拦截了。最好的例子是 CSRF 跨站攻击原理,请求是发送到了后端服务器无论是否跨域!注意:有些浏览器不允许从 HTTPS 的域跨域访问 HTTP,比如  Chrome 和 Firefox,这些浏览器在请求还未发出的时候就会拦截请求,这是一个特例。)

跨域资源共享( CORS )机制允许 Web 应用服务器进行跨域访问控制,从而使跨域数据传输得以安全进行。浏览器支持在 API 容器中(例如 XMLHttpRequest 或 Fetch )使用 CORS,以降低跨域 HTTP 请求所带来的风险。

这篇文章适用于网站管理员、后端和前端开发者。CORS 需要客户端和服务器同时支持。目前,所有浏览器都支持该机制。 对于服务端的支持,开发者可以阅读补充材料 cross-origin sharing from a server perspective (with PHP code snippets) 。

跨域资源共享标准( cross-origin sharing standard )允许在下列场景中使用跨域 HTTP 请求:

  • 前文提到的由 XMLHttpRequest 或 Fetch 发起的跨域 HTTP 请求。
  • Web 字体 (CSS 中通过 @font-face 使用跨域字体资源), 因此,网站就可以发布 TrueType 字体资源,并只允许已授权网站进行跨站调用。
  • WebGL 贴图
  • 使用 drawImage 将 Images/video 画面绘制到 canvas
  • 样式表(使用 CSSOM)
  • Scripts (未处理的异常)

本文概述了跨域资源共享机制及其所涉及的 HTTP 首部字段。

概述

跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。

接下来的内容将讨论相关场景,并剖析该机制所涉及的 HTTP 首部字段。

若干访问控制场景

这里,我们使用三个场景来解释跨域资源共享机制的工作原理。这些例子都使用 XMLHttpRequest 对象。

本文中的 JavaScript 代码片段都可以从 http://arunranga.com/examples/access-control/ 获得。另外,使用支持跨域  XMLHttpRequest 的浏览器访问该地址,可以看到代码的实际运行结果。

关于服务端对跨域资源共享的支持的讨论,请参见这篇文章: Server-Side_Access_Control (CORS)。

简单请求

某些请求不会触发 CORS 预检请求。本文称这样的请求为“简单请求”,请注意,该术语并不属于 Fetch (其中定义了 CORS)规范。若请求满足所有下述条件,则该请求可视为“简单请求”:

  • 使用下列方法之一:
    • GET
    • HEAD
    • POST
  • Fetch 规范定义了对 CORS 安全的首部字段集合,不得人为设置该集合之外的其他首部字段。该集合为:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type (需要注意额外的限制)
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
  •  Content-Type 的值属于下列之一:
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain
注意: 这些跨域请求与浏览器发出的其他跨域请求并无二致。如果服务器未返回正确的响应首部,则请求方不会收到任何数据。因此,那些不允许跨域请求的网站无需为这一新的 HTTP 访问控制特性担心。
注意: WebKit Nightly 和 Safari Technology Preview 为Accept, Accept-Language, 和 Content-Language 首部字段的值添加了额外的限制。如果这些首部字段的值是“非标准”的,WebKit/Safari 就不会将这些请求视为“简单请求”。WebKit/Safari 并没有在文档中列出哪些值是“非标准”的,不过我们可以在这里找到相关讨论:Require preflight for non-standard CORS-safelisted request headers Accept, Accept-Language, and Content-Language, Allow commas in Accept, Accept-Language, and Content-Language request headers for simple CORS, and Switch to a blacklist model for restricted Accept headers in simple CORS requests。其它浏览器并不支持这些额外的限制,因为它们不属于规范的一部分。

比如说,假如站点 http://foo.example 的网页应用想要访问 http://bar.other 的资源。http://foo.example 的网页中可能包含类似于下面的 JavaScript 代码:

var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/public-data/';
   
function callOtherDomain() {
  if(invocation) {    
    invocation.open('GET', url, true);
    invocation.onreadystatechange = handler;
    invocation.send(); 
  }
}

客户端和服务器之间使用 CORS 首部字段来处理跨域权限:

分别检视请求报文和响应报文:

GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Referer: http://foo.example/examples/access-control/simpleXSInvocation.html
Origin: http://foo.example


HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2.0.61 
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml

[XML Data]

第 1~10 行是请求首部。第10行 的请求首部字段 Origin 表明该请求来源于 http://foo.exmaple

第 13~22 行是来自于 http://bar.other 的服务端响应。响应中携带了响应首部字段 Access-Control-Allow-Origin(第 16 行)。使用 OriginAccess-Control-Allow-Origin 就能完成最简单的访问控制。本例中,服务端返回的 Access-Control-Allow-Origin: * 表明,该资源可以被任意外域访问。如果服务端仅允许来自 http://foo.example 的访问,该首部字段的内容如下:

Access-Control-Allow-Origin: http://foo.example

现在,除了 http://foo.example,其它外域均不能访问该资源(该策略由请求首部中的 ORIGIN 字段定义,见第10行)。Access-Control-Allow-Origin 应当为 * 或者包含由 Origin 首部字段所指明的域名。

预检请求

与前述简单请求不同,“需预检的请求”要求必须首先使用 OPTIONS   方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。"预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。

当请求满足下述任一条件时,即应首先发送预检请求:

  • 使用了下面任一 HTTP 方法:
    • PUT
    • DELETE
    • CONNECT
    • OPTIONS
    • TRACE
    • PATCH
  • 人为设置了对 CORS 安全的首部字段集合之外的其他首部字段。该集合为:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type (but note the additional requirements below)
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
  •  Content-Type 的值不属于下列之一:
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain

注意: WebKit Nightly 和 Safari Technology Preview 为Accept, Accept-Language, 和 Content-Language 首部字段的值添加了额外的限制。如果这些首部字段的值是“非标准”的,WebKit/Safari 就不会将这些请求视为“简单请求”。WebKit/Safari 并没有在文档中列出哪些值是“非标准”的,不过我们可以在这里找到相关讨论:Require preflight for non-standard CORS-safelisted request headers Accept, Accept-Language, and Content-Language, Allow commas in Accept, Accept-Language, and Content-Language request headers for simple CORS, and Switch to a blacklist model for restricted Accept headers in simple CORS requests。其它浏览器并不支持这些额外的限制,因为它们不属于规范的一部分。

如下是一个需要执行预检请求的 HTTP 请求:

var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/post-here/';
var body = '<?xml version="1.0"?><person><name>Arun</name></person>';
    
function callOtherDomain(){
  if(invocation)
    {
      invocation.open('POST', url, true);
      invocation.setRequestHeader('X-PINGOTHER', 'pingpong');
      invocation.setRequestHeader('Content-Type', 'application/xml');
      invocation.onreadystatechange = handler;
      invocation.send(body); 
    }
}

......

上面的代码使用 POST 请求发送一个 XML 文档,该请求包含了一个自定义的请求首部字段(X-PINGOTHER: pingpong)。另外,该请求的 Content-Type 为 application/xml。因此,该请求需要首先发起“预检请求”。

OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type


HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

预检请求完成之后,发送实际请求:

POST /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
X-PINGOTHER: pingpong
Content-Type: text/xml; charset=UTF-8
Referer: http://foo.example/examples/preflightInvocation.html
Content-Length: 55
Origin: http://foo.example
Pragma: no-cache
Cache-Control: no-cache

<?xml version="1.0"?><person><name>Arun</name></person>


HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:40 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://foo.example
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 235
Keep-Alive: timeout=2, max=99
Connection: Keep-Alive
Content-Type: text/plain

[Some GZIP'd payload]

浏览器检测到,从 JavaScript 中发起的请求需要被预检。从上面的报文中,我们看到,第 1~12 行发送了一个使用 OPTIONS 方法的“预检请求”。 OPTIONS 是 HTTP/1.1 协议中定义的方法,用以从服务器获取更多信息。该方法不会对服务器资源产生影响。 预检请求中同时携带了下面两个首部字段:

Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER

首部字段 Access-Control-Request-Method 告知服务器,实际请求将使用 POST 方法。首部字段 Access-Control-Request-Headers 告知服务器,实际请求将携带两个自定义请求首部字段:X-PINGOTHER 与 Content-Type。服务器据此决定,该实际请求是否被允许。

第14~26 行为预检请求的响应,表明服务器将接受后续的实际请求。重点看第 17~20 行:

Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400

首部字段 Access-Control-Allow-Methods 表明服务器允许客户端使用 POST, GET 和 OPTIONS 方法发起请求。该字段与 HTTP/1.1 Allow: response header 类似,但仅限于在需要访问控制的场景中使用。

首部字段 Access-Control-Allow-Headers 表明服务器允许请求中携带字段 X-PINGOTHER 与 Content-Type。与 Access-Control-Allow-Methods 一样,Access-Control-Allow-Headers 的值为逗号分割的列表。

最后,首部字段 Access-Control-Max-Age 表明该响应的有效时间为 86400 秒,也就是 24 小时。在有效时间内,浏览器无须为同一请求再次发起预检请求。请注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将不会生效。

预检请求与重定向

大多数浏览器不支持针对于预检请求的重定向。如果一个预检请求发生了重定向,浏览器将报告错误:

The request was redirected to 'https://example.com/foo', which is disallowed for cross-origin requests that require preflight

Request requires preflight, which is disallowed to follow cross-origin redirect

CORS 最初要求该行为,不过在后续的修订中废弃了这一要求。

在浏览器的实现跟上规范之前,有两种方式规避上述报错行为:

  • 在服务端去掉对预检请求的重定向;
  • 将实际请求变成一个简单请求。

如果上面两种方式难以做到,我们仍有其他办法:

  • 使用简单请求模拟预检请求,用以探查预检请求是否重定向到其他 URL(使用  Response.url 或 XHR.responseURL);
  • 向上一步中获得的 URL 发起请求。

不过,如果请求由于缺失 Authorization 字段而引起一个预检请求,则这一方法将无法使用。这种情况只能由服务端进行更改。

附带身份凭证的请求

Fetch 与 CORS 的一个有趣的特性是,可以基于  HTTP cookies 和 HTTP 认证信息发送身份凭证。一般而言,对于跨域 XMLHttpRequest 或 Fetch 请求,浏览器不会发送身份凭证信息。如果要发送凭证信息,需要设置 XMLHttpRequest 的某个特殊标志位。

本例中,http://foo.example 的某脚本向 http://bar.other 发起一个GET 请求,并设置 Cookies:

var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/credentialed-content/';
    
function callOtherDomain(){
  if(invocation) {
    invocation.open('GET', url, true);
    invocation.withCredentials = true;
    invocation.onreadystatechange = handler;
    invocation.send(); 
  }
}

第 7 行将 XMLHttpRequest 的 withCredentials 标志设置为 true,从而向服务器发送 Cookies。因为这是一个简单 GET 请求,所以浏览器不会对其发起“预检请求”。但是,如果服务器端的响应中未携带 Access-Control-Allow-Credentials: true ,浏览器将不会把响应内容返回给请求的发送者。

客户端与服务器端交互示例如下:

GET /resources/access-control-with-credentials/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Referer: http://foo.example/examples/credential.html
Origin: http://foo.example
Cookie: pageAccess=2


HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:34:52 GMT
Server: Apache/2.0.61 (Unix) PHP/4.4.7 mod_ssl/2.0.61 OpenSSL/0.9.7e mod_fastcgi/2.4.2 DAV/2 SVN/1.4.2
X-Powered-By: PHP/5.2.6
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Credentials: true
Cache-Control: no-cache
Pragma: no-cache
Set-Cookie: pageAccess=3; expires=Wed, 31-Dec-2008 01:34:53 GMT
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 106
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain


[text/plain payload]

即使第 11 行指定了 Cookie 的相关信息,但是,如果 bar.other 的响应中缺失 Access-Control-Allow-Credentials: true(第 19 行),则响应内容不会返回给请求的发起者。

附带身份凭证的请求与通配符

对于附带身份凭证的请求,服务器不得设置 Access-Control-Allow-Origin 的值为“*”。

这是因为请求的首部中携带了 Cookie 信息,如果 Access-Control-Allow-Origin 的值为“*”,请求将会失败。而将 Access-Control-Allow-Origin 的值设置为 http://foo.example,则请求将成功执行。

另外,响应首部中也携带了 Set-Cookie 字段,尝试对 Cookie 进行修改。如果操作失败,将会抛出异常。

HTTP 响应首部字段

本节列出了规范所定义的响应首部字段。上一小节中,我们已经看到了这些首部字段在实际场景中是如何工作的。

Access-Control-Allow-Origin

响应首部中可以携带一个 Access-Control-Allow-Origin 字段,其语法如下:

Access-Control-Allow-Origin: <origin> | *

其中,origin 参数的值指定了允许访问该资源的外域 URI。对于不需要携带身份凭证的请求,服务器可以指定该字段的值为通配符,表示允许来自所有域的请求。

例如,下面的字段值将允许来自 http://mozilla.com 的请求:

Access-Control-Allow-Origin: http://mozilla.com

如果服务端指定了具体的域名而非“*”,那么响应首部中的 Vary 字段的值必须包含 Origin。这将告诉客户端:服务器对不同的源站返回不同的内容。

Access-Control-Expose-Headers

Access-Control-Expose-Headers 首部字段指定了服务端允许的首部字段集合。

Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header

服务器允许请求中携带 X-My-Custom-Header 和 X-Another-Custom-Header 这两个字段。

Access-Control-Max-Age

Access-Control-Max-Age 首部字段指明了预检请求的响应的有效时间。

Access-Control-Max-Age: <delta-seconds>

delta-seconds 表示该响应在多少秒内有效。

Access-Control-Allow-Credentials

Access-Control-Allow-Credentials 首部字段用于预检请求的响应,表明服务器是否允许 credentials 标志设置为 true 的请求。请注意:简单 GET 请求不会被预检;如果对此类请求的响应中不包含该字段,浏览器不会将响应返回给请求的调用者。

Access-Control-Allow-Credentials: true

上文已经讨论了附带身份凭证的请求。

Access-Control-Allow-Methods

Access-Control-Allow-Methods 首部字段用于预检请求的响应。其指明了实际请求所允许使用的 HTTP 方法。

Access-Control-Allow-Methods: <method>[, <method>]*

相关示例见这里。

Access-Control-Allow-Headers

Access-Control-Allow-Headers 首部字段用于预检请求的响应。其指明了实际请求中允许携带的首部字段。

Access-Control-Allow-Headers: <field-name>[, <field-name>]*

HTTP 请求首部字段

本节列出了可用于发起跨域请求的首部字段。请注意,这些首部字段无须手动设置。 当开发者使用 XMLHttpRequest 对象发起跨域请求时,它们已经被设置就绪。

Origin

Origin 首部字段表明预检请求或实际请求的源站。

Origin: <origin>

origin 参数的值为源站 URI。它不包含任何路径信息,只是服务器名称。

Note: 有时候将该字段的值设置为空字符串是有用的,例如,当源站是一个 data URL 时。

注意,不管是否为跨域请求,ORIGIN 字段总是被发送。

Access-Control-Request-Method

Access-Control-Request-Method 首部字段用于预检请求。其作用是,将实际请求所使用的 HTTP 方法告诉服务器。

Access-Control-Request-Method: <method>

相关示例见这里。

Access-Control-Request-Headers

Access-Control-Request-Headers 首部字段用于预检请求。其作用是,将实际请求所携带的首部字段告诉服务器。

Access-Control-Request-Headers: <field-name>[, <field-name>]*

相关示例见这里。

规范

SpecificationStatusComment
Fetch
CORS
Living StandardNew definition; supplants CORS specification.
CORSRecommendationInitial definition.

浏览器兼容性

  • Desktop
  • Mobile
FeatureChromeFirefox (Gecko)Internet ExplorerOperaSafari
Basic support43.58 (via XDomainRequest)
10
124

注:

  • IE 10 提供了对规范的完整支持,但在较早版本(8 和 9)中,CORS 机制是借由 XDomainRequest 对象完成的。
  • Firefox 3.5 引入了对 XMLHttpRequests 和 Web 字体的跨域支持(但最初的实现并不完整,这在后续版本中得到完善);Firefox 7 引入了对 WebGL 贴图的跨域支持;Firefox 9 引入了对 drawImage 的跨域支持。

毛笔的分类

毛笔的分类主要有依尺寸,还有笔毛的种类,来源,形状等等来分。­

依尺寸可以简单的把毛笔分为:小楷,中楷,大楷。­

依笔毛的种类可分为:软毫,硬毫,兼毫等。­

 

依来源可分为:胎毛笔、狼毛笔、兔肩紫毫笔、鹿毛笔、鸡毛笔、鸭毛笔、羊毛笔、猪毛笔、鼠毛笔、虎毛笔、黄牛耳毫笔等。­

羊毫类:笔头是用山羊毛制成的。羊毫笔比较柔软,吸墨量大,适于写表现圆浑厚实的点画。比狼毫笔经久耐用。此类笔以湖笔为多,价格比较便宜。一般常见的有大楷笔、京提(或称提笔)、联锋、屏锋、顶锋、盖锋、条幅、玉笋、玉兰蕊、京楂等。 ­

狼毫笔:笔头是用黄鼠狼尾巴上的毛制成的。以东北产的鼠尾为最,称"北狼毫"、"关东辽尾"。狼毫比羊毫笔力劲挺,宜书宜画,但不如羊毫笔耐用,价格也比羊毫贵。常见的品种有兰竹、写意、山水、花卉、叶筋、衣纹、红豆、小精工、鹿狼毫书画(狼毫中加入鹿毫制成)、豹狼毫(狼毫中加入豹毛制成的)、特制长峰狼毫,超品长峰狼毫等。 ­

紫毫:笔头是以兔毛制成的,因色泽紫黑光亮而得名。此种笔挺拔尖锐而锋利,弹性比狼毫更强,以安徽出产的野兔毛为最好。 ­

兼毫:笔头是用两种刚柔不同的动物毛制成的。常见的种类有羊狼兼毫、羊紫兼毫,如五紫五羊,七紫三羊等等。此种笔的优点兼具了羊狼毫笔的长处,刚柔适中,价格也适中,为书画家常用。种类有调和式、心被式。 ­

 

依形状可分为:圆毫,尖豪等。­

 

14661028_1

 

此外,根据笔锋的长短,毛笔又有[B]长锋、中锋、短锋之别,性能各异。长锋容易画出婀娜多姿的线条,短锋容易使线条凝重厚实,中锋则兼而有之,画山水以用中锋为宜。根据笔锋的大小不同,毛笔又分为小、中、大等型号。画山水各种型号都要准备一点,一般“小山水”(小狼毫)、“大山水”(大狼毫)各备一枝,“小白云”、“大白云”羊毫笔各备一枝,再有一支更大的羊毫“斗笔”就可以了。新笔锋多尖锐,只适宜画细线,皴、擦、点擢用旧笔效果好。有的画家喜用秃笔,点线别有苍劲朴拙之趣。好的毛笔,都具有圆、齐、尖、健四个特点,使用起来运转自如。画笔用后要及时冲洗干净,避免墨汁干结损坏。 ­

制作毛笔笔管的原料一般有:金、银、象牙、玻璃、紫檀、斑竹。

14661028_2

 

画家论笔

 

* 文震亨:“尖”、“齐”、“圆”、“健”,笔之四德。­

* 张大千:张大千说英国出的一种水彩笔十分名贵,是用英国某地黄牛耳朵内的细毛制成,2500头黄牛才出一磅黄牛耳毫。牛耳毫制成的画笔,吸水饱满,有弹性。­

* 潘天寿:羊毫圆细柔训,含水量强,笔锋出水慢,运用枯墨湿墨,有其特长。­

 

我国的制笔,历史上有宣笔(安徽宣城)、湖笔(浙江湖州)两大中心。现在上海、苏州、北京、成都等地生产的画笔也享有盛誉。目前出产毛笔最有名的地方在浙江省吴兴县善琏镇湖州,称为湖笔。­

14661028_3

 

笔有“四德”,即“尖、齐、圆、健”,下面逐一介绍: ­

尖:指笔毫聚拢时,末端要尖瑞。笔尖则写字锋棱易出,较易传神。作家常以“秃笔”称自己的笔,但笔不尖则成秃笔,做书神采顿失。 ­

选购新笔时,毫毛有胶聚合,很容易分辨。在检查旧笔时,先将笔润湿,毫毛聚拢,便可分辨尖秃。 ­

齐:指笔尖润开压平后,毫尖平齐。毫若齐则压平时长短相等,中无空隙,运笔时“万毫齐力”。因为需把笔完全润开,选购时就较无法检查这一点。 ­

圆:指笔毫圆满如枣核之形,就是毫毛充足的意思。如毫毛充足则书写时笔力完足,反之则身瘦,缺乏笔力。笔锋圆满,运笔自能圆转如意。 ­

选购时,毫毛有胶聚拢,是不是圆满,仔细看看就知道了。 ­

健:即笔腰弹力;将笔毫重压后提起,随即恢复原状。笔有弹力,则能运用自如;一般而言,兔毫、狼毫弹力较羊毫强,书亦坚挺峻拔。 ­

14661028_6

 墨的用法

学习书法绘画,笔法与墨法互为依存,相得益彰,正所谓“墨法之少,全从笔出”。用墨直接影响到作品的神采。历代书家无不深究墨法,清代包世臣在《艺舟双楫》中说:“书法字法,本寸笔,成于墨,则墨法尤书芝一大关键已。”明代文人画兴起,国画的墨法融进书法,增添了书法作品的笔情墨趣。

浓墨是最主要的一种墨法。墨色浓黑,书写时行笔实而沉,墨不浮,能人纸,具有凝重沉稳,神采外耀的效果。古代书家颜真卿、苏轼都喜用浓墨。苏东坡对用墨的要求是:“光清不浮,湛湛然如小儿一睛,”认为用墨光而不黑,失掉了墨的作用;黑而不光则“索然无神气”。细观苏轼的墨迹,有浓墨淋漓的艺术效果。清代刘墉用墨亦浓重。书风貌丰骨劲,有“浓墨宰相”之称、与浓墨相反的便是淡墨。

淡墨介于黑白色之间,呈灰色调,给人以清远淡雅的美感。叫代的其昌善用淡墨,书法追求萧散意境。从作品通篇观来,浓淡变化丰富,空灵剔透,清静雅致,仙住所著《画禅室随笔》中说:“用墨须使有;闰,不可使其枯燥,尤忌浓肥,肥则大恶道矣。”清代的上文治被誉为“淡墨探花”,书法源出于董香光,传其风神,作品疏秀占淡。其实,川浓淡墨各有风韵,关键在掌握,用墨过淡则伤神采;太浓刚弊于无锋。正如清代周星莲所说:“用墨之法,浓欲其活,淡欲其华活与华,非墨宽不可。不善用墨者,浓则易枯,淡则近薄,不数年间,已奄奄无生气矣,”

涨墨是指过量的墨水在宣纸上溢出笔画之外的现象。涨墨在“墨不旁出”的正统墨法观念上是不成立的。然而涨墨之妙正在于既保持笔画的基本形态,又有朦胧的墨趣,线面交融。王铎擅用涨墨,以用墨扩大了线条的表现层次,作品中干淡浓湿结合,墨色丰富,一扫前人呆板的墨法,形成了强烈的视觉艺术效果。黄宾虹对墨法研究更有独到之处,提出了“五笔墨”的理论。他偶尔将涨墨法应用于篆书创作中,又表现出一番奇趣。

渴笔、枯笔分别指运笔中墨水所含的水分或墨大多失去后在纸上行笔的效果。渴笔苍中见润泽;枯笔苍中见老辣。在书写中应用渴笔、枯笔二法,应控制墨量适宜。宋代米芾的手札《经宿帖》“本欲来日送,月明,遂今夕送耳;”几字,以渴笔、枯笔表现,涩笔力行、苍健雄劲。

书法的墨法表现技巧十分丰富,用水是表现各朴墨法的关键。《画谭》说:“墨法在用水,以墨为形,水为气,气行,形乃活矣。占入水墨并称,实有至理”。另外,用墨的技巧还与笔法的提按轻重,纸质的优劣密切相关。一幅书法作品的墨色变化,会增强作品的韵律美。当然,墨法的运用贵有自然,切不可盲目为追求某种墨法效果而堕入俗境。

 

古人论画时讲用墨有四个要素

 

一是“活”,落笔爽利,讲究墨色滋润自然;

二是“鲜”,墨色要灵秀焕发、清新可人;

三是“变幻”,虚实结合,变化多样;

四是“笔墨一致”,笔墨相互映发,调和一致。以此移证于书法的用墨也应是有一定的启迪作用。

 

磨墨的方法是要用清水,若水中混有杂质,则磨出来的墨就不纯了。至于加水,最先不宜过多,以免将墨浸软,或墨汁四溅,以逐渐加入为宜。磨墨要用力平均,慢慢地磨研,磨到墨汁浓稠为止。用墨要新鲜现磨,磨好了而时间放得太久的墨称为宿墨,宿墨一般是不可用的。但也有画家喜用宿墨作画,那只是个别的。

 

墨的使用应注意以下几点

 

墨正:柳公权有所谓的“笔正”,磨墨也是如此,心正墨亦正,墨若不正偏斜,既不雅观,磨出的墨也不均匀。         力匀而急缓适中∶磨墨时用力过轻过重,太急太缓,墨汁皆必粗而不匀。用力过轻,速度太缓,浪费时间且墨浮;用力过重,速度过急,则墨粗而生沬,色亦无光。正确的方法应该是“指按推用力”,轻重有节,切莫太急。

浓度适中:书画作品中即使是淡笔,也是用浓墨写的,差别是在蘸墨的多寡,而不是墨的浓淡。如果墨汁含水过多,笔一下到纸上便迅速扩散,形成一团团大小不一、形态各异的墨团,怎么有笔画可言?但也别矫枉过正了,拿浓到像半凝果冻的墨来写字也是很可怕的。还需记得用洁白纸,以浓墨为佳,若用有色纸,则可以稍淡。

随磨随用:用墨必需新磨,因墨汁若放置一日以上,胶与煤逐渐脱离,墨光既乏光彩,又不能持久,故以宿墨作书,极易褪色。而市面上所售的现成墨汁,有些胶重滞笔,有些则浓度太低,落纸极易化开,防腐剂又多,易损笔锋,不宜采用。

储放匣内:研墨完毕,即将墨取出,不可置放砚池,否则胶易黏着砚池,乾后不易取下,且可防潮湿变软,两败俱伤。也不可以曝放阳光下,以免干燥。所以最好还是放在匣内,即可防湿,又避免阳光直射,不染尘,是最好方法。

JTA 深度历险-原理与实现

利用 JTA 处理事务

什么是事务处理

事务是计算机应用中不可或缺的组件模型,它保证了用户操作的原子性 ( Atomicity )、一致性 ( Consistency )、隔离性 ( Isolation ) 和持久性 ( Durabilily )。关于事务最经典的示例莫过于信用卡转账:将用户 A 账户中的 500 元人民币转移到用户 B 的账户中,其操作流程如下
1. 将 A 账户中的金额减少 500
2. 将 B 账户中的金额增加 500
这两个操作必须保正 ACID 的事务属性:即要么全部成功,要么全部失败;假若没有事务保障,用户的账号金额将可能发生问题:
假如第一步操作成功而第二步失败,那么用户 A 账户中的金额将就减少 500 元而用户 B 的账号却没有任何增加(不翼而飞);同样如果第一步出错 而第二步成功,那么用户 A 的账户金额不变而用户 B 的账号将增加 500 元(凭空而生)。上述任何一种错误都会产生严重的数据不一致问题,事务的缺失对于一个稳定的生产系统是不可接受的。

J2EE 事务处理方式

1. 本地事务:紧密依赖于底层资源管理器(例如数据库连接 ),事务处理局限在当前事务资源内。此种事务处理方式不存在对应用服务器的依赖,因而部署灵活却无法支持多数据源的分布式事务。在数据库连接中使用本地事务示例如下:

清单 1. 本地事务处理实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public void transferAccount() {
        Connection conn = null;
        Statement stmt = null;
        try{
            conn = getDataSource().getConnection();
            // 将自动提交设置为 false,
            //若设置为 true 则数据库将会把每一次数据更新认定为一个事务并自动提交
            conn.setAutoCommit(false);
           
            stmt = conn.createStatement();
            // 将 A 账户中的金额减少 500
            stmt.execute("\
            update t_account set amount = amount - 500 where account_id = 'A'");
            // 将 B 账户中的金额增加 500
            stmt.execute("\
            update t_account set amount = amount + 500 where account_id = 'B'");
           
            // 提交事务
            conn.commit();
            // 事务提交:转账的两步操作同时成功
        } catch(SQLException sqle){           
            try{
                // 发生异常,回滚在本事务中的操做
               conn.rollback();
                // 事务回滚:转账的两步操作完全撤销
                stmt.close();
                conn.close();
            }catch(Exception ignore){
               
            }
            sqle.printStackTrace();
        }
    }

2. 分布式事务处理 : Java 事务编程接口(JTA:Java Transaction API)和 Java 事务服务 (JTS;Java Transaction Service) 为 J2EE 平台提供了分布式事务服务。分布式事务(Distributed Transaction)包括事务管理器(Transaction Manager)和一个或多个支持 XA 协议的资源管理器 ( Resource Manager )。我们可以将资源管理器看做任意类型的持久化数据存储;事务管理器承担着所有事务参与单元的协调与控制。JTA 事务有效的屏蔽了底层事务资源,使应用可以以透明的方式参入到事务处理中;但是与本地事务相比,XA 协议的系统开销大,在系统开发过程中应慎重考虑是否确实需要分布式事务。若确实需要分布式事务以协调多个事务资源,则应实现和配置所支持 XA 协议的事务资源,如 JMS、JDBC 数据库连接池等。使用 JTA 处理事务的示例如下(注意:connA 和 connB 是来自不同数据库的连接)

清单 2. JTA 事务处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public void transferAccount() {
       
        UserTransaction userTx = null;
        Connection connA = null;
        Statement stmtA = null;
               
        Connection connB = null;
        Statement stmtB = null;
   
        try{
              // 获得 Transaction 管理对象
            userTx = (UserTransaction)getContext().lookup("\
                  java:comp/UserTransaction");
            // 从数据库 A 中取得数据库连接
            connA = getDataSourceA().getConnection();
           
            // 从数据库 B 中取得数据库连接
            connB = getDataSourceB().getConnection();
     
                       // 启动事务
            userTx.begin();
           
            // 将 A 账户中的金额减少 500
            stmtA = connA.createStatement();
            stmtA.execute("
           update t_account set amount = amount - 500 where account_id = 'A'");
           
            // 将 B 账户中的金额增加 500
            stmtB = connB.createStatement();
            stmtB.execute("\
            update t_account set amount = amount + 500 where account_id = 'B'");
           
            // 提交事务
            userTx.commit();
            // 事务提交:转账的两步操作同时成功(数据库 A 和数据库 B 中的数据被同时更新)
        } catch(SQLException sqle){
           
            try{
                  // 发生异常,回滚在本事务中的操纵
                 userTx.rollback();
                // 事务回滚:转账的两步操作完全撤销
                //( 数据库 A 和数据库 B 中的数据更新被同时撤销)
               
                stmt.close();
                conn.close();
                ...
            }catch(Exception ignore){
               
            }
            sqle.printStackTrace();
           
        } catch(Exception ne){
            e.printStackTrace();
        }
    }

JTA 实现原理

很多开发人员都会对 JTA 的内部工作机制感兴趣:我编写的代码没有任何与事务资源(如数据库连接)互动的代码,但是我的操作(数据库更新)却实实在在的被包含在了事务中,那 JTA 究竟是通过何种方式来实现这种透明性的呢? 要理解 JTA 的实现原理首先需要了解其架构:它包括事务管理器(Transaction Manager)和一个或多个支持 XA 协议的资源管理器 ( Resource Manager ) 两部分, 我们可以将资源管理器看做任意类型的持久化数据存储;事务管理器则承担着所有事务参与单元的协调与控制。 根据所面向对象的不同,我们可以将 JTA 的事务管理器和资源管理器理解为两个方面:面向开发人员的使用接口(事务管理器)和面向服务提供商的实现接口(资源管理器)。其中开发接口的主要部分即为上述示例中引用的 UserTransaction 对象,开发人员通过此接口在信息系统中实现分布式事务;而实现接口则用来规范提供商(如数据库连接提供商)所提供的事务服务,它约定了事务的资源管理功能,使得 JTA 可以在异构事务资源之间执行协同沟通。以数据库为例,IBM 公司提供了实现分布式事务的数据库驱动程序,Oracle 也提供了实现分布式事务的数据库驱动程序, 在同时使用 DB2 和 Oracle 两种数据库连接时, JTA 即可以根据约定的接口协调者两种事务资源从而实现分布式事务。正是基于统一规范的不同实现使得 JTA 可以协调与控制不同数据库或者 JMS 厂商的事务资源,其架构如下图所示:

图 1. JTA 体系架构

JTA 体系架构图

开发人员使用开发人员接口,实现应用程序对全局事务的支持;各提供商(数据库,JMS 等)依据提供商接口的规范提供事务资源管理功能;事务管理器( TransactionManager )将应用对分布式事务的使用映射到实际的事务资源并在事务资源间进行协调与控制。 下面,本文将对包括 UserTransaction、Transaction 和 TransactionManager 在内的三个主要接口以及其定义的方法进行介绍。

面向开发人员的接口为 UserTransaction (使用方法如上例所示),开发人员通常只使用此接口实现 JTA 事务管理,其定义了如下的方法:

  • begin()- 开始一个分布式事务,(在后台 TransactionManager 会创建一个 Transaction 事务对象并把此对象通过 ThreadLocale 关联到当前线程上 )
  • commit()- 提交事务(在后台 TransactionManager 会从当前线程下取出事务对象并把此对象所代表的事务提交)
  • rollback()- 回滚事务(在后台 TransactionManager 会从当前线程下取出事务对象并把此对象所代表的事务回滚)
  • getStatus()- 返回关联到当前线程的分布式事务的状态 (Status 对象里边定义了所有的事务状态,感兴趣的读者可以参考 API 文档 )
  • setRollbackOnly()- 标识关联到当前线程的分布式事务将被回滚

面向提供商的实现接口主要涉及到 TransactionManager 和 Transaction 两个对象

Transaction 代表了一个物理意义上的事务,在开发人员调用 UserTransaction.begin() 方法时 TransactionManager 会创建一个 Transaction 事务对象(标志着事务的开始)并把此对象通过 ThreadLocale 关联到当前线程。UserTransaction 接口中的 commit()、rollback(),getStatus() 等方法都将最终委托给 Transaction 类的对应方法执行。Transaction 接口定义了如下的方法:

  • commit()- 协调不同的事务资源共同完成事务的提交
  • rollback()- 协调不同的事务资源共同完成事务的回滚
  • setRollbackOnly()- 标识关联到当前线程的分布式事务将被回滚
  • getStatus()- 返回关联到当前线程的分布式事务的状态
  • enListResource(XAResource xaRes, int flag)- 将事务资源加入到当前的事务中(在上述示例中,在对数据库 A 操作时 其所代表的事务资源将被关联到当前事务中,同样,在对数据库 B 操作时其所代表的事务资源也将被关联到当前事务中)
  • delistResourc(XAResource xaRes, int flag)- 将事务资源从当前事务中删除
  • registerSynchronization(Synchronization sync)- 回调接口,Hibernate 等 ORM 工具都有自己的事务控制机制来保证事务, 但同时它们还需要一种回调机制以便在事务完成时得到通知从而触发一些处理工作,如清除缓存等。这就涉及到了 Transaction 的回调接口 registerSynchronization。工具可以通过此接口将回调程序注入到事务中,当事务成功提交后,回调程序将被激活。

TransactionManager 本身并不承担实际的事务处理功能,它更多的是充当用户接口和实现接口之间的桥梁。下面列出了 TransactionManager 中定义的方法,可以看到此接口中的大部分事务方法与 UserTransaction 和 Transaction 相同。 在开发人员调用 UserTransaction.begin() 方法时 TransactionManager 会创建一个 Transaction 事务对象(标志着事务的开始)并把此对象通过 ThreadLocale 关联到当前线程上;同样 UserTransaction.commit() 会调用 TransactionManager.commit(), 方法将从当前线程下取出事务对象 Transaction 并把此对象所代表的事务提交, 即调用 Transaction.commit()

  • begin()- 开始事务
  • commit()- 提交事务
  • rollback()- 回滚事务
  • getStatus()- 返回当前事务状态
  • setRollbackOnly()
  • getTransaction()- 返回关联到当前线程的事务
  • setTransactionTimeout(int seconds)- 设置事务超时时间
  • resume(Transaction tobj)- 继续当前线程关联的事务
  • suspend()- 挂起当前线程关联的事务

在系统开发过程中会遇到需要将事务资源暂时排除的操作,此时就需要调用 suspend() 方法将当前的事务挂起:在此方法后面所做的任何操作将不会被包括在事务中,在非事务性操作完成后调用 resume()以继续事务(注: 要进行此操作需要获得 TransactionManager 对象, 其获得方式在不同的 J2EE 应用服务器上是不一样的)
下面将通过具体的代码向读者介绍 JTA 实现原理。下图列出了示例实现中涉及到的 Java 类,其中 UserTransactionImpl 实现了 UserTransaction 接口,TransactionManagerImpl 实现了 TransactionManager 接口,TransactionImpl 实现了 Transaction 接口

图 2. JTA 实现类图

Transaction 类图

清单 3. 开始事务 - UserTransactionImpl implenments UserTransaction
1
2
3
4
public void begin() throws NotSupportedException, SystemException {
   // 将开始事务的操作委托给 TransactionManagerImpl
   TransactionManagerImpl.singleton().begin();
     }
清单 4. 开始事务 - TransactionManagerImpl implements TransactionManager
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 此处 transactionHolder 用于将 Transaction 所代表的事务对象关联到线程上
private static ThreadLocal<TransactionImpl> transactionHolder
        = new ThreadLocal<TransactionImpl>();
    
     //TransacationMananger 必须维护一个全局对象,因此使用单实例模式实现
     private static TransactionManagerImpl singleton = new TransactionManagerImpl();
    
     private TransactionManagerImpl(){
        
     }
    
     public static TransactionManagerImpl singleton(){
         return singleton;
     }
     public void begin() throws NotSupportedException, SystemException {
         //XidImpl 实现了 Xid 接口,其作用是唯一标识一个事务
         XidImpl xid = new XidImpl();
         // 创建事务对象,并将对象关联到线程
         TransactionImpl tx = new TransactionImpl(xid);
        
         transactionHolder.set(tx);
     }

现在我们就可以理解 Transaction 接口上没有定义 begin 方法的原因了:Transaction 对象本身就代表了一个事务,在它被创建的时候就表明事务已经开始,因此也就不需要额外定义 begin() 方法了。

清单 5. 提交事务 - UserTransactionImpl implenments UserTransaction
1
2
3
4
5
6
7
8
9
10
11
12
13
14
    public void commit() throws RollbackException, HeuristicMixedException,
        HeuristicRollbackException, SecurityException,
        IllegalStateException, SystemException {
       
        // 检查是否是 Roll back only 事务,如果是回滚事务
           if(rollBackOnly){
            rollback();
       
            return;
          } else {
           // 将提交事务的操作委托给 TransactionManagerImpl
           TransactionManagerImpl.singleton().commit();
          }
}
清单 6. 提交事务 - TransactionManagerImpl implenments TransactionManager
1
2
3
4
5
6
7
8
public void commit() throws RollbackException, HeuristicMixedException,
    HeuristicRollbackException, SecurityException,
    IllegalStateException, SystemException {
                
     // 取得当前事务所关联的事务并通过其 commit 方法提交
     TransactionImpl tx = transactionHolder.get();
     tx.commit();
             }

同理, rollback、getStatus、setRollbackOnly 等方法也采用了与 commit() 相同的方式实现。 UserTransaction 对象不会对事务进行任何控制, 所有的事务方法都是通过 TransactionManager 传递到实际的事务资源即 Transaction 对象上。
上述示例演示了 JTA 事务的处理过程,下面将为您展示事务资源(数据库连接,JMS)是如何以透明的方式加入到 JTA 事务中的。首先需要明确的一点是,在 JTA 事务 代码中获得的数据库源 ( DataSource ) 必须是支持分布式事务的。在如下的代码示例中,尽管所有的数据库操作都被包含在了 JTA 事务中,但是因为 MySql 的数据库连接是通过本地方式获得的,对 MySql 的任何更新将不会被自动包含在全局事务中。

清单 7. JTA 事务处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public void transferAccount() {
       
        UserTransaction userTx = null;
        Connection mySqlConnection = null;
        Statement mySqlStat = null;
               
        Connection connB = null;
        Statement stmtB = null;
   
        try{
               // 获得 Transaction 管理对象
            userTx =
           (UserTransaction)getContext().lookup("java:comp/UserTransaction");
            // 以本地方式获得 mySql 数据库连接
            mySqlConnection = DriverManager.getConnection("localhost:1111");
           
            // 从数据库 B 中取得数据库连接, getDataSourceB 返回应用服务器的数据源
            connB = getDataSourceB().getConnection();
     
                       // 启动事务
            userTx.begin();
           
            // 将 A 账户中的金额减少 500
            //mySqlConnection 是从本地获得的数据库连接,不会被包含在全局事务中
            mySqlStat = mySqlConnection.createStatement();
            mySqlStat.execute("
            update t_account set amount = amount - 500 where account_id = 'A'");
           
            //connB 是从应用服务器得的数据库连接,会被包含在全局事务中
            stmtB = connB.createStatement();
            stmtB.execute("
            update t_account set amount = amount + 500 where account_id = 'B'");
           
            // 事务提交:connB 的操作被提交,mySqlConnection 的操作不会被提交
            userTx.commit();
        } catch(SQLException sqle){
            // 处理异常代码
        } catch(Exception ne){
            e.printStackTrace();
        }
    }

为什么必须从支持事务的数据源中获得的数据库连接才支持分布式事务呢?其实支持事务的数据源与普通的数据源是不同的,它实现了额外的 XADataSource 接口。我们可以简单的将 XADataSource 理解为普通的数据源(继承了 java.sql.PooledConnection),只是它为支持分布式事务而增加了 getXAResource 方法。另外,由 XADataSource 返回的数据库连接与普通连接也是不同的,此连接除了实现 java.sql.Connection 定义的所有功能之外还实现了 XAConnection 接口。我们可以把 XAConnection 理解为普通的数据库连接,它支持所有 JDBC 规范的数据库操作,不同之处在于 XAConnection 增加了对分布式事务的支持。通过下面的类图读者可以对这几个接口的关系有所了解:

图 3. 事务资源类图

Transaction 类图

应用程序从支持分布式事务的数据源获得的数据库连接是 XAConnection 接口的实现,而由此数据库连接创建的会话(Statement)也为了支持分布式事务而增加了功能,如下代码所示:

清单 8. JTA 事务资源处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public void transferAccount() {
       
        UserTransaction userTx = null;
               
        Connection conn = null;
        Statement stmt = null;
   
        try{
               // 获得 Transaction 管理对象
            userTx = (UserTransaction)getContext().lookup("
            java:comp/UserTransaction");
            // 从数据库中取得数据库连接, getDataSourceB 返回支持分布式事务的数据源
            conn = getDataSourceB().getConnection();
                       // 会话 stmt 已经为支持分布式事务进行了功能增强
            stmt = conn.createStatement();
           
                       // 启动事务
            userTx.begin();
            stmt.execute("update t_account ... where account_id = 'A'");
            userTx.commit();
        } catch(SQLException sqle){
            // 处理异常代码
        } catch(Exception ne){
            e.printStackTrace();
        }
    }

我们来看一下由 XAConnection 数据库连接创建的会话(Statement)部分的代码实现(不同的 JTA 提供商会有不同的实现方式,此处代码示例只是向您演示事务资源是如何被自动加入到事务中)。 我们以会话对象的 execute 方法为例,通过在方法开始部分增加对 associateWithTransactionIfNecessary 方法的调用,即可以保证在 JTA 事务期间,对任何数据库连接的操作都会被透明的加入到事务中。

清单 9. 将事务资源自动关联到事务对象 - XAStatement implements Statement
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public void execute(String sql) {
               // 对于每次数据库操作都检查此会话所在的数据库连接是否已经被加入到事务中
        associateWithTransactionIfNecessary();
        try{
                     // 处理数据库操作的代码
             ....
        } catch(SQLException sqle){
            // 处理异常代码
        } catch(Exception ne){
            e.printStackTrace();
        }
    }
public void associateWithTransactionIfNecessary(){
        
        // 获得 TransactionManager
        TransactionManager tm = getTransactionManager();
               Transaction tx = tm.getTransaction();
            // 检查当前线程是否有分布式事务
           if(tx != null){
            // 在分布式事务内,通过 tx 对象判断当前数据连接是否已经被包含在事务中,
            //如果不是那么将此连接加入到事务中
            Connection conn = this.getConnection();
            //tx.hasCurrentResource, xaConn.getDataSource() 不是标准的 JTA
                       // 接口方法,是为了实现分布式事务而增加的自定义方法
            if(!tx.hasCurrentResource(conn)){
                XAConnection xaConn = (XAConnection)conn;
                XADataSource xaSource = xaConn.getDataSource();
                   
                // 调用 Transaction 的接口方法,将数据库事务资源加入到当前事务中
                tx.enListResource(xaSource.getXAResource(), 1);
                }
            }
       }

XAResource 与 Xid: XAResource 是 Distributed Transaction Processing: The XA Specification 标准的 Java 实现,它是对底层事务资源的抽象,定义了分布式事务处理过程中事务管理器和资源管理器之间的协议,各事务资源提供商(如 JDBC 驱动,JMS)将提供此接口的实现。使用此接口,开发人员可以通过自己的编程实现分布式事务处理,但这些通常都是由应用服务器实现的(服务器自带实现更加高效,稳定) 为了说明,我们将举例说明他的使用方式。
在使用分布式事务之前,为了区分事务使之不发生混淆,必须实现一个 Xid 类用来标识事务,可以把 Xid 想象成事务的一个标志符,每次在新事务创建是都会为事务分配一个 Xid,Xid 包含三个元素:formatID、gtrid(全局事务标识符)和 bqual(分支修饰词标识符)。 formatID 通常是零,这意味着你将使用 OSI CCR(Open Systems Interconnection Commitment, Concurrency 和 Recovery 标准)来命名;如果你要使用另外一种格式,那么 formatID 应该大于零,-1 值意味着 Xid 为无效。

gtrid 和 bqual 分别包含 64 个字节二进制码来分别标识全局事务和分支事务, 唯一的要求是 gtrid 和 bqual 必须是全局唯一的。
XAResource 接口中主要定义了如下方法:

  • commit()- 提交事务
  • isSameRM(XAResource xares)- 检查当前的 XAResource 与参数是否同一事务资源
  • prepare()- 通知资源管理器准备事务的提交工作
  • rollback()- 通知资源管理器回滚事务

在事务被提交时,Transaction 对象会收集所有被当前事务包含的 XAResource 资源,然后调用资源的提交方法,如下代码所示:

清单 10. 提交事务 - TransactionImpl implements Transaction
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void commit() throws RollbackException, HeuristicMixedException,
             HeuristicRollbackException, SecurityException,
             IllegalStateException, SystemException {
            
             // 得到当前事务中的所有事务资源
                List<XAResource> list = getAllEnlistedResouces();
            
             // 通知所有的事务资源管理器,准备提交事务
                        // 对于生产级别的实现,此处需要进行额外处理以处理某些资源准备过程中出现的异常
             for(XAResource xa : list){
                 xa.prepare();
             }
            
             // 所有事务性资源,提交事务
             for(XAResource xa : list){
                 xa.commit();
             }
       }

结束语

通过如上介绍相信读者对 JTA 的原理已经有所了解,本文中的示例代码都是理想情况下的假设实现。一款完善成熟的 JTA 事务实现需要考虑与处理的细节非常多,如性能(提交事务的时候使用多线程方式并发提交事务)、容错(网络,系统异常)等, 其成熟也需要经过较长时间的积累。感兴趣的读者可以阅读一些开源 JTA 实现以进一步深入学习。

来源:  https://www.ibm.com/developerworks/cn/java/j-lo-jta/index.html

docker搭建mongo集群

收集到篇关于docker下安装mongodb的文章, 原文地址在后面给出, 记录在这里。

一. 准备工作

  • 安装docker
  • 安装docker-compose

注意

  1. docker通过unix socket通信, 需要sudo权限, 每次操作docker都需要在命令行前添加sudo, 比较烦, 官方给出了方法.安装docker是会创建docker用户组, 也拥有unix socker读写权限, 将当前用户添加至docker用户组, 即可免除命令行前添加sudo. 参考
  2. 容器间默认通过网桥连接内外部网络, 主机需要允许网卡转发
  3. docker 网桥默认网段为172.17.42.1/16, 公司vpn默认对网段172.17.0.0/20路由, 会覆盖docker网段, 导致容器无法与外部网络通信, 修改/etc/default/docker, 添加DOCKER_OPTS="--bip=192.168.1.0/20", 这里网段是容器间的内部网络, 可以自行修改, 改到满意.
  4. 国内使用官方镜像比较慢,建议使用国内第三方镜像加速。如:DaoCloud,阿里云

二. 实验

(一) 主从模式

mongodb主从模式
mongodb主从模式
  • 准备工作
    1. 创建db目录 mkdir -p /data/mongodbtest/master /data/mongodbtest/slaver
    2. `docker-compose.yml内容如下:
      version: '2'
      services:
        master:
          image: mongo:latest
          volumes:
            - /data/mongodbtest/master:/data/db
          command: mongod --dbpath /data/db  --master
        slaver:
          image: mongo:latest
          volumes:
            - /data/mongodbtest/slaver:/data/db
          command: mongod --dbpath /data/db --slave --source master:27017
          links:
           - master
  • 启动容器docker-compose up -d

    注: docker-compose会默认将文件夹名字作为容器名的前缀, 我的文件夹为masterslave, 容器明分别为masterslave_master_1masterslave_slave_1

  • 验证
    在master中插入一条记录:

    docker-compose exec master mongo
    use test
    db.test.insert({msg: "from master", ts: new Date()})

    查看slave中的数据, 执行

    docker-compose exec slaver mongo
    rs.slaveOk()
    use test
    db.test.find()
    db.test.insert({msg: 'from slaver', ts: new Date()})  //报错, slaver只有读权限

    查看slave服务信息

    db.printReplicationInfo()

    测试故障转移:首先, 关闭master, docker-compose stop master; 其次, 重新连接slave,查看服务信息, 插入数据.如下:

    docker-compose exec slaver mongo
    db.printReplicationInfo() //依然是slave, 没有自动切换为master
    use test
    db.testData.insert({msg: "from slave", ts: new Date()}) //插入失败
  • 总结
    简单的master-slave模型仅仅做了一个数据复制, 而且并不可靠, master挂了整体将无法进行写操作

(二) 副本集(Relica set)

  • 介绍

三实例

1主 + 2次要
mongodb主从复制
mongodb主从复制
  • 准备工作
    1. 创建db文件夹
      mkdir -p /data/mongodbtest/replset/rs1 /data/mongodbtest/replset/rs2 /data/mongodbtest/replset/rs2
    2. docker-compose.yml
      version: '3'
      services:
        rs1:
          image: mongo:latest
          volumes:
            - /data/mongodbtest/replset/rs1:/data/db
          command: mongod --dbpath /data/db --replSet myset
        rs2:
          image: mongo:latest
          volumes:
            - /data/mongodbtest/replset/rs2:/data/db
          command: mongod --dbpath /data/db --replSet myset
        rs3:
          image: mongo:2.7.8
          volumes:
            - /data/mongodbtest/replset/rs3:/data/db
          command: mongod --dbpath /data/db --replSet myset
  • 初始化副本级
    docker-compose exec rs1 mongo
    rs.initiate()
    rs.add('rs2:27017')
    rs.add('rs3:27017')
    rs.conf() //查看配置
    rs.status() //查看副本级状态
  • 验证
    • 数据复制功能
      docker-compose exec rs1 mongo
      use test
      db.test.insert({msg: 'from primary', ts: new Date()})
      quit()
      docker-compose exec rs2 mongo
      rs.slaveOk() //副本集默认仅primary可读写
      use test
      db.test.find()
      quit()
      docker-compose exec rs3 mongo
      rs.slaveOk() //副本集默认仅primary可读写
      use test
      db.test.find()
      quit()
    • 故障转移功能
      副本集在 primary 挂掉以后, 可以在 secondary 中选取出新的 primary

      docker-compose stop rs1

      登录 rs2/rs3 中查看可以看到,选出了新的 primary ,这时候我们重新启动 rs1,它成为了 secondary

      docker-compose start rs1
      docker-compose exec rs1 mongo
  • 总结
    通过客户端的设置, 可以进行主副节点读写分离:

    a). primary:默认参数, 只从主节点上进行读取操作;
    b). primaryPreferred:大部分从主节点上读取数据, 只有主节点不可用时从secondary节点读取数据;
    c). secondary:只从secondary节点上进行读取操作, 存在的问题是secondary节点的数据会比primary节点数据"旧";
    d). secondaryPreferred:优先从secondary节点进行读取操作, secondary节点不可用时从主节点读取数据;
    e). nearest:不管是主节点,secondary节点, 从网络延迟最低的节点上读取数据.

    副节点不是越多越好, 因为引主节点做写操作, 其他副节点从主节点复制数据, 副节点越多, 主节点压力越大.

1主 + 1副 + 1仲裁
mongodb主从仲裁
mongodb主从仲裁
  • 准备工作
    1. 删除/data/mongodbtest/replset/rs[1-3]/ 下的内容
    2. docker-compose.yml
      version: '3'
      services:
        rs1:
          image: mongo:latest
          volumes:
            - /data/mongodbtest/replset/rs1:/data/db
          command: mongod --dbpath /data/db --replSet myset --oplogSize 128
        secondary:
          image: mongo:latest
          volumes:
            - /data/mongodbtest/replset/rs2:/data/db
          command: mongod --dbpath /data/db --replSet myset --oplogSize 128
        arbiter:
          image: mongo:latest
          command: mongod --dbpath /data/db --replSet myset --smallfiles --oplogSize 128
  • 启动
    docker-compose up -d
  • 初始化副本级
    mongo rs1:27017  
    rs.initiate()
    rs.add('rs2:27017')
    rs.add('rs3:27017', true) // arbiter only
    rs.conf() //查看配置
    rs.status() //查看副本级状态
  • 验证(同上)
  • 总结
    每个副本集对主节点都是全量拷贝, 数量压力增大的时候, 节点压力随之变大. 无法自动扩张.

限制

在官方文档中提到了副本集的一些限制:

  • 一个副本集成员不能超过12个
  • 副本集最多有50个成员
  • 可以投票的成员最多7个
  • 如果启动 mongod 的时候没有通过 --oplogSize 制定 oplog 的大小,默认 oplog 最大为 50G

分片

mongodb集群分片
mongodb集群分片

MongoDB 提供了水平扩展的功能,其 Sharding 机制使其具备了支撑大数据量和大吞吐量的能力。
在一个 Sharding 集群中,有下面三种角色:

  • shard: 每个 shard 存储整个 sharding 集群数据的一个子集,每一个 shard 都是一个 replset
  • mongos: 查询路由,客户端通过其从 shard 中查询数据,也可以理解为 proxy
  • config servers: 配置服务器,存储整个 sharding 集群的元数据和配置。

    MongoDB 3.4 起,要求 config servers 也是 replset
    MongoDB 提供两种分片策略:hash shardingrange sharding,需要根据自己的业务特征和数据特征进行选择。

测试版

 

ms6
  • 环境
    1 config servers(csrs) + 1 mongos + 1shard(1primary + 1secondary + 1arbiter)
  • 准备工作
    1. 创建文件夹
      mkdir -p /data/mongodbtest/cs/rs1 /data/mongodbtest/cs/rs2 /data/mongodbtest/cs/rs3 # for config server replset
      mkdir -p /data/mongodbtest/sh/rs1 /data/mongodbtest/sh/rs2 /data/mongodbtest/sh/rs3 # for sharding 1
      mkdir -p /data/mongodbtest/mongos
    2. keyfile
      MongoDB 规定 sharding 集群内部必须有授权机制,比如 mongos 去访问 config server的时候,我们这里通过key
    3. docker-compose.yml
      version: '3'
      services:
        csrs1:
          image: mongo:latest
          volumes:
            - /data/mongodbtest/cs/rs1:/data/db
          command: mongod --noauth --configsvr --replSet csrs --dbpath /data/db
        csrs2:
          image: mongo:latest
          volumes:
            - /data/mongodbtest/cs/rs2:/data/db
          command: mongod --noauth --configsvr --replSet csrs --dbpath /data/db
        csrs3:
          image: mongo:latest
          volumes:
            - /data/mongodbtest/cs/rs3:/data/db
          command: mongod --noauth --configsvr --replSet csrs --dbpath /data/db
        mongos:
          image: mongo:latest
          command: mongos --noauth --configdb csrs/csrs1:27019,csrs2:27019,csrs3:27019
        shrs1:
          image: mongo:latest
          volumes:
            - /data/mongodbtest/sh/rs1:/data/db
          command: mongod --noauth --dbpath /data/db --shardsvr --replSet shrs
        shrs2:
          image: mongo:latest
          volumes:
            - /data/mongodbtest/sh/rs2:/data/db
          command: mongod --noauth --dbpath /data/db --shardsvr --replSet shrs
        shrs3:
          image: mongo:latest
          volumes:
            - /data/mongodbtest/sh/rs3:/data/db
          command: mongod --noauth --dbpath /data/db --shardsvr --replSet shrs
  • 启动 docker-compose up -d
  • 配置副本集
    1. 配置 config server
      docker-compose exec csrs1 mongo --port 27019
      rs.initiate()
      
      rs.add('csrs2:27019')
      rs.add('csrs3:27019')
      rs.status() //查看状态
      quit()

      config server 默认端口为 27019

    2. 配置 shard server
      docker-compose exec shrs1 mongo --port 27018
      rs.initiate()
      var cfg = rs.conf()
      cfg.members[0].host = 'shrs1:27018'
      rs.reconfig(cfg)
      rs.add('shrs2:27018')
      rs.add('shrs3:27018')
      quit()

      'shard server' 默认端口号为 27018

  • 配置 mongos
    docker-compose exec mongos mongo
    sh.addShard('shrs/shrs1:27018')
    sh.status() //查看状态

操作记录

文章来源:   http://www.jianshu.com/p/4d86e9c33e0d