不为有趣之事,何遣有涯之生
不失其所者久,死而不亡者寿

Elasticsearch简明教程(1)

hzqiuxm阅读(24)评论(0)

ElasticSearch简介与安装入门

ElasticSearch简介

概述

Elasticsearch是一个基于Lucene实现的、(准)实时、分布式的全文搜索和分析引擎。

准实时,意味着有轻微的延迟(通常为1秒)就可以从入库建索引文件到能够进行关键字搜索。

作用

ES主要提供全文搜索、结构化搜索以及分析的功能,并能将这三者混合使用

特性

  • 支持RESTful风格的接口
  • 输入输出支持JSON风格
  • 分布式索引、搜索
  • 索引自动分片、负载均衡
  • 自动发现机器、组建集群
  • 高性能、高可扩展性、高可用提供复制机制
  • 使用简单,快速上手

ElasticSearch 安装

1:去官网下载最新的版本:https://www.elastic.co/products/elasticsearch,这里用的是目前最新版6.4.1
2:Windows下直接解压后就能使用
3:在CentOS上安装ES
(1)解压,然后拷贝到你要放置的位置
(2)ES在linux上不能用root启动,创建ES的用户和组:
groupadd es
useradd es -g es -p es

(3)把ES安装的文件夹的所属用户和组修改为上面创建的用户和组:chown -R es:es elasticsearch-2.3.4
(4)切换用户到es,然后就可以启动ES了: su es
(5)如果想要外部能访问,需要修改es绑定的network.host地址为你安装的服务器地址,想要后台运行,可以用-d

检查是否安装成功:访问host:9200,例如:http://192.168.52.128:9200/
看到返回以下类似内容就表示安装成功了:

{
  "name" : "iYLxRzi",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "SXIJqTdrRDiI2p5enqO7Dw",
  "version" : {
    "number" : "5.6.8",
    "build_hash" : "688ecce",
    "build_date" : "2018-02-16T16:46:30.010Z",
    "build_snapshot" : false,
    "lucene_version" : "6.6.1"
  },
  "tagline" : "You Know, for Search"
}

启动中可能出现的错误参考下面几个博文来进行解决,链接如下:
https://blog.csdn.net/u012371450/article/details/51776505
https://www.jianshu.com/p/4c6f9361565b
https://www.cnblogs.com/yidiandhappy/p/7714481.html

常用插件安装

Head

1:直接到ES安装路径下的bin里,运行
./plugin install mobz/elasticsearch-head
2:打开http://server的ip:9200/_plugin/head/ 就可以看效果了

注意:6开头的版本后面不支持命令行安装了,不要参考以上操作
6开头版本以上的请参考:
http://www.mamicode.com/info-detail-2105773.html
https://blog.csdn.net/dyangel2013/article/details/79504516

安装好node后几个关键命令:

npm install -g grunt-cli
grunt -version 检测下安装成功没
wget  https://github.com/mobz/elasticsearch-head/archive/master.zip
unzip master.zip
npm install -g cnpm --registry=https://registry.npm.taobao.org  //安装依赖的时候用国内淘宝的镜像比较快
nohup grunt server & //后台启动,elasticsearch-head-master目录下
//elasticsearch.yml中最后添加
http.cors.enabled: true
http.cors.allow-origin: "*"

head插件展示示意图:

IK分词器

默认的分词器standard对中文分词效果不好,只是把所有中文字一个个分开而已
1:下载对应版本的ik:https://github.com/medcl/elasticsearch-analysis-ik

版本v5.5.1之前的

然后自己编译打包,生成jar包,需要修改一下pom文件,把最下面的

<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>

全部注释掉
2:把下载到ik解压到plugins
3:把生成的jar拷贝到plugins/elasticsearch-analysis-ik-master下面,同时还要拷入需要的依赖jar,commons-codec-1.9.jar、commons-logging-1.2.jar、httpclient-4.4.1.jar、httpcore-4.4.1.jar
4:在ik源码的main/resources里面,拷贝plugin-descriptor.properties到plugins/elasticsearch-analysis-ik-master下面,然后把里面的参数数据修改一下,参考如下:

description=ik_analyzer
version=1.9.4
name=ik_analyzer
site=false
jvm=true
classname=org.elasticsearch.plugin.analysis.ik.AnalysisIkPlugin
java.version=1.8
elasticsearch.version=2.3.4
isolated=false

5:修改es的config/elasticsearch.yml,在最后添加:

index.analysis.analyzer.ik.type : 'ik'
index.analysis.analyzer.default.tokenizer : 'ik'

然后就可以按照ik官方给的测试进行测试了

版本v5.5.1之后的

直接参考官方示例即可,安装很方便一个命令即可。注意插件的版本号和Elastic的版本号要一致。

./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.4.1/elasticsearch-analysis-ik-6.4.1.zip
IKAnalyzer.cfg.xml配置说明
<?xml version="1.0" encoding="UTF-8"?> 
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"> 
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict">custom/mydict.dic;custom/single_word_low_freq.dic</entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords">custom/ext_stopword.dic</entry>
<!--用户可以在这里配置远程扩展字典 -->
<entry key="remote_ext_dict">location</entry>
<!--用户可以在这里配置远程扩展停止词字典-->
    <entry key="remote_ext_stopwords">http://xxx.com/xxx.dic</entry></properties>

注意事项

请确保你的扩展词典的文本格式为 UTF8 编码,每个词以换行符相隔。
IK 分词从 5.0.0 版本开始使用 ik_smart 和 ik_max_word 两种分词方式

  • ikmaxword:表示最细粒度拆分。优点是查询效果比较好。缺点是会产生很多碎片,对于大文本字段不建议使用 ik_max_word

例子:
将“中华人民共和国国歌”拆分为“中华人民共和国,中华人民,中华,华人,人民共和国,人民,人,民,共和国,共和,和,国国,国歌”,会穷尽各种可能的组合

  • ik_smart:表示最粗粒度拆分,优点是降低了索引存储。缺点是查询效果不好
    例:将“中华人民共和国国歌”拆分为“中华人民共和国,国歌”。这个时候输入“中华”是匹配不到的,只能匹配“中华人民共和国”或“国歌”。

pinyin分词器

对于很多的搜索场景,用户输入的有时候并非汉字,可能是拼音或者拼音首字母,这个时候我们同样要匹配到数据,就需要引入 pinyin 分词器。

./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-pinyin/releases/download/v6.4.1/elasticsearch-analysis-pinyin-6.4.1.zip

mavel

1: 在ES里面安装Marvel插件

./plugin install license
./plugin install marvel-agent

2:安装Kibana,解压然后拷贝到要放置的位置即可修改一下配置文件里面的elasticsearch.url
3:在Kibana里面安装Marvel插件
./kibana plugin --install elasticsearch/marvel/latest
4: 启动ES和Kibana
5: 然后就可以到http://server的ip:5601/app/marvel

简单官网示例测试步骤

  • 创建索引
curl -XPUT http://192.168.0.57:9200/index 
  • 创建映射
curl -XPOST http://192.168.0.57:9200/index/fulltext/_mapping -H 'Content-Type:application/json' -d'
{
        "properties": {
            "content": {
                "type": "text",
                "analyzer": "ik_max_word",
                "search_analyzer": "ik_max_word"
            }
        }

}'
  • 创建数据
curl -XPOST http://192.168.0.57:9200/index/fulltext/1 -H 'Content-Type:application/json' -d'
{"content":"美国留给伊拉克的是个烂摊子吗"}
'


curl -XPOST http://192.168.0.57:9200/index/fulltext/2 -H 'Content-Type:application/json' -d'
{"content":"公安部:各地校车将享最高路权"}
'

curl -XPOST http://192.168.0.57:9200/index/fulltext/3 -H 'Content-Type:application/json' -d'
{"content":"中韩渔警冲突调查:韩警平均每天扣1艘中国渔船"}
'
curl -XPOST http://192.168.0.57:9200/index/fulltext/4 -H 'Content-Type:application/json' -d'
{"content":"中国驻洛杉矶领事馆遭亚裔男子枪击 嫌犯已自首"}
'
  • 执行查询
curl -XPOST http://192.168.0.57:9200/index/fulltext/_search  -H 'Content-Type:application/json' -d'
{
    "query" : { "match" : { "content" : "中国" }},
    "highlight" : {
        "pre_tags" : ["<tag1>", "<tag2>"],
        "post_tags" : ["</tag1>", "</tag2>"],
        "fields" : {
            "content" : {}
        }
    }
}
'
  • 返回结果
{
    "took": 14,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "failed": 0
    },
    "hits": {
        "total": 2,
        "max_score": 2,
        "hits": [
            {
                "_index": "index",
                "_type": "fulltext",
                "_id": "4",
                "_score": 2,
                "_source": {
                    "content": "中国驻洛杉矶领事馆遭亚裔男子枪击 嫌犯已自首"
                },
                "highlight": {
                    "content": [
                        "<tag1>中国</tag1>驻洛杉矶领事馆遭亚裔男子枪击 嫌犯已自首 "
                    ]
                }
            },
            {
                "_index": "index",
                "_type": "fulltext",
                "_id": "3",
                "_score": 2,
                "_source": {
                    "content": "中韩渔警冲突调查:韩警平均每天扣1艘中国渔船"
                },
                "highlight": {
                    "content": [
                        "均每天扣1艘<tag1>中国</tag1>渔船 "
                    ]
                }
            }
        ]
    }
}

Docker简明教程(11)

hzqiuxm阅读(68)评论(0)

Docker容器链接与编排

容器链接

前面学习到的端口映射,并不是唯一把docker连接到另一个容器的方法。

docker有一个连接系统允许将多个容器连接在一起,共享连接信息。
docker连接会创建一个父子关系,其中父容器可以看到子容器的信息。
有时出于安全原因,可以强制docker只允许有连接的容器之间互相通信,可以在启动docker守护进程的时候,加上--icc=false,关闭没有连接的容器间通信。

在docker run的时候, 指定--link :标志创建了两个容器间的父子连接,这样容器就不用暴露端口了,大大增加安全性。

基本使用

基本语法:--link 要连接的容器的名字:这个链接的别名

1:执行连接需要依靠容器的名字,因此创建每一个容器的时候,请使用--name来命名。

注意:容器的名称必须是唯一的。如果想使用重复的名称来命名容器,需要使用docker rm命令删除以前的容器。

2:被连接的容器必须运行在同一个Docker宿主机上
示例:

docker run --name db -e MYSQL_ROOT_PASSWORD=123321qq -d mysql
docker run -d -p 9080:8080 --name web --link db:dblink cctomcat:9.0

3:可以通过docker inspect查看里面的Links,如: "/db:/web/dblink"
如果启动的时候,出现类似如下的错误:

COMMAND_FAILED: '/usr/sbin/iptables -w2 -t nat -A DOCKER -p tcp -d 0/0 --dport
9080 -j DNAT --to-destination 172.17.0.4:8080 ! -i docker0' failed:

这可能是网络问题造成,解决方法如下:
1:首先先验证docker容器内部网络是否能ping通宿主机如果能ping通,即可通过重建docker0网络恢复
2:先停掉宿主机上运行的docker容器,然后执行以下命令

iptables -t nat -F
ifconfig docker0 down
brctl delbr docker0

3:重启docker服务

使用容器连接来通信

最简单的方法就是在子容器里面,也就是web里面,直接使用link的别名来代替具体的host或者是ip地址,比如:
jdbc:mysql://dblink:3306/mydb

容器编排

编排简介

Docker的最佳实践建议:一个容器只运行一个进程。而实际的应用会由多个组件构成,要运行多个组件就需要运行多个容器,这就需要对这多个容器进行编排。
所谓编排:主要就是多个docker容器的自动配置、协作和管理服务的过程。Docker提供了docker-compose工具来实现。

Docker-compose简介

compose是用来定义和运行一个或多个容器应用的工具,使用python开发,通过yml文件来定义多个容器应用,非常适合在单机环境下部署一个或多个容器,并自动把多个容器互相关联起来。

其实,docker-compose做的就相当于解析配置文件,然后按照配置去执行一系列的docker命令。

Docker-compose安装

官方安装文档:https://docs.docker.com/compose/install/

Docker-compose基本示例

1:准备好要启动的镜像,虽然可以直接在compose里面build镜像,建议还是先准备好
2:编写docker-compose.yml
3:然后就docker-compose up -d,启动就好了
4:docker-compose.yml示例如下:

version: '2'
services:
  mysqldb:
    image: 'mysql:5.7'
    environment:
     - MYSQL_ROOT_PASSWORD=123321qq
    volumes:
     - /home/dev/mysqldata:/var/lib/mysql
    privileged: true
web:
  image: 'cctomcat:9.0'
  ports:
   - "9080:8080"
  volumes:
   - /home/dev/tomcat9docker/webapps/test:/usr/local/tomcat/webapps/test
  privileged: true
  links:
   - mysqldb:dblink

Docker-compose yml文件的配置

1:一份标准配置文件可以包含version、services、networks 三大部分,详细的参照指南见官方网站:https://docs.docker.com/compose/compose-file/
2:version目前是有1,2,3这么三个
3:srvices常见的配置有:
(1)服务名称:用来表示一个服务,自定义的
(2)image:指定服务的镜像名称或镜像ID。如果镜像在本地不存在,Compose 将会尝试拉取这个镜像,Build和image必须使用一个。
(3)build:服务除了可以基于指定的镜像,还可以基于一份Dockerfile,在使用up 启动之时执行构建任务,这个构建标签就是build,它可以指定Dockerfile 所在文件夹的路径。Compose 将会利用它自动构建这个镜像,然后使用这个镜像启动服务容器。如果你同时指定了image 和build 两个标签,那么Compose 会构建镜像并且把镜像命名为image 后面的那个名字。
(4)args:类似Dockerfile 中的ARG 指令,可以在构建过程中指定环境变量,构建成功后取消
(5)command:使用command 可以覆盖容器启动后默认执行的命令
(6)container_name:自定义容器的名称
(7)links:指定与其它容器的连接,与Docker client的--link一样效果
(8)volumes:将host主机上的路径或文件,挂载到容器中
(9)ports:将host主机的端口映射到容器的某个端口
(10)environment:设置环境变量, 与Dockerfile 中的ENV 指令一样会把变量一直保存在镜像、容器中,类似docker run -e 的效果
(11)privileged:设置挂载目录的权限
(12)depends_on:一般项目容器启动的顺序是有要求的,可以用depends_on来解决容器的依赖、启动先后的问题。

Docker-compose 的networks配置

容器间的通讯,除了使用--link外,现在更推荐使用自定义网络,然后利用服务名进行通讯。每个自定义网络都可以配置很多东西,包括网络所使用的驱动、网络地址范围等设置。例如:

networks:
frontend:
backend:

1:你会看到frontend、backend后面是空的,这是指一切都使用默认,换句话说,在单机环境中,将意味着使用bridge 驱动;而在Swarm 环境中,使用overlay 驱动,而且地址范围完全交给Docker 引擎决定。

2:然后在每个services配置里面,也有一个networks,用来指定服务要连接到哪些网络上,可以指定多个,例如:

services:
  nginx:
...
  networks:
   - frontend
web:
...
  networks:
   - frontend
   - backend
mysql:
...
  networks:
   - backend

3:连接到同一个网络的容器,可以进行互连;而不同网络的容器则会被隔离。
4:处于同一网络的容器,可以使用服务名访问对方
5:给前面的例子添加networks的配置,如下:

version: '2'
services:
  mysqldb:
    image: 'mysql:latest'
    environment:
     - MYSQL_ROOT_PASSWORD=cc
    volumes:
     - /home/dev/mysqldata:/var/lib/mysql
    privileged: true
    networks:
     - frontend
web:
  image: 'cctomcat:9.0'
  ports:
   - "9080:8080"
  volumes:
   - /home/dev/tomcat9docker/webapps/test:/usr/local/tomcat/webapps/test
  privileged: true
  links:
   - mysqldb:dblink
  networks:
   - frontend
networks:
  frontend:
  backend:

Docker简明教程(10)

hzqiuxm阅读(86)评论(0)

Docker与常见的数据库结合使用

MYSQL使用

1:下载镜像

docker pull mysql:5.7 

2:指定宿主机数据卷启动

docker run --name mysql -p 12345:3306 -v /home/dev/mysqldata:/var/lib/mysql --privileged=true -e MYSQL_ROOT_PASSWORD=123321qq -d mysql:5.7 

这里指定了使用自己的mysql数据文件

3:使用自定义配置文件启动,在mysqlconf下放着my.cnf文件:

docker run --name mysql -p 12345:3306 -v /home/dev/mysqldata:/var/lib/mysql -v /home/dev/mysqlconf:/etc/mysql/conf.d --privileged=true -e MYSQL_ROOT_PASSWORD=123321qq -d mysql:5.7

4:使用自定义日志目录

docker run --name mysql -p 12345:3306 -v /home/dev/mysqldata:/var/lib/mysql -v /home/dev/mysqllogs:/var/log/mysql --privileged=true -e MYSQL_ROOT_PASSWORD=123321qq -d mysql:5.7 

如果日志开启不成功,可能是因为mysql用户没有对日志文件夹的操作权限,进入到容器里面,设置一下,示例如下:

chown -R mysql:mysql /var/log/mysql

5:数据表备份,在宿主机上执行

docker exec 容器id sh -c 'exec mysqldump --all-databases -uroot -p"123321qq"' > /home/all-databases.sql 

REDIS使用

1.下载镜像

docker pull redis 

2.启动容器,默认暴露6379端口

docker run --name myredis -d redis

3.如果想使用自己的配置文件启动redis,一种方法是在其基础上写一个dockerfile,例如:

FROM redis
COPY redis.conf /usr/local/etc/redis/redis.conf
CMD [ "redis-server", "/usr/local/etc/redis/redis.conf" ]

另外一种方式是在启动命令中修改配置,如:

docker run -v /myredis/conf/redis.conf:/usr/local/etc/redis/redis.conf --name myredis redis redis-server /usr/local/etc/redis/redis.conf

4.数据默认存储在VOLUME /data目录下,使用-v来指定挂载,如:

docker run --name myreis -d -p 6379:6379 -v /redisdocker/data:/data -v /redisconf/redis.conf:/usr/local/etc/redis/redis.conf --privileged=true redis redis-server /usr/local/etc/redis/redis.conf

注意:自己写的conf文件里面,不要配置bind的ip,也不要daemonize的配置,直接注释掉

5.aof的持久化方式

如果需要开启aof的持久化方式默认是rdb的,可以在配置文件里面设置,也可通过命令行指定:

docker run --name some-redis -d redis redis-server --appendonly yes 

6.如果应用需要连接redis:

docker run --name some-app --link some-redis:redis -d application-that-uses-redis

ElasticSearch使用

1.下载镜像

docker pull elasticsearch:6.4.1

2.启动过后,在里面安装上ik和head,然后构建自己的镜像:

具体安装过程请查看 [ELK简明指南系统—Elasticsearch及常用插件安装]一文

docker run -d -v /es-6.4.1/data:/usr/share/elasticsearch/data -v /es-6.4.1/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml -v /es-6.4.1/config/logging.yml:/usr/share/elasticsearch/config/logging.yml -v /es-6.4.1/config/scripts:/usr/share/elasticsearch/config/scripts --privileged=true -p 9200:9200 -p 9300:9300 elasticsearch:6.4.1
docker commit -m="add head and ik" -a="qiuxm" 容器id qiuxmes:6.4.1

3.然后就可以使用自己的镜像了

注意在使用docker里面的es的时候,要把client.transport.sniff设置为false

Docker简明教程(9)

hzqiuxm阅读(107)评论(0)

Docker优雅使用指南

敏捷思想地图

敏捷思想和团队

Docker 的优点在于通过简化CI(持续集成)、CD(持续交付)的构建流程,让开发者把更多的精力用在开发上。

每家公司都有自己的开发技术栈,我们需要结合实际情况对其进行持续改进,优化自己的构建流程。
那在引入 Docker 技术的时候,遵循什么样的业界标准呢?大家会发现其实目前并没有什么最佳实践标准,常常以最佳实践为口号,引入多种工具链,导致在使用 Docker 的过程中没有侧重点。涉及到 Docker 选型,又在工具学习上花费大量时间,而不是选用合适的工具以组建可持续发布产品的开发团队。基于这样的场景,我们可以把“简单易用”的原则作为评判标准。

首先指定好整体构建蓝图,让团队的每一个成员都清晰知道流程,然后需要解决的是让团队成员尽快掌握 Docker 命令行的使用。

基础镜像

包括了操作系统命令行和类库的最小集合,一旦启用,所有应用都需要以它为基础创建应用镜像。Ubuntu 作为官方使用的默认版本,是目前最易用的版本,但系统没有经过优化,可以考虑使用第三方优化过的版本,比如 phusion-baseimage。
那我们为什么要使用优化过的基础镜像?拿baseimage来说,它提供了:

  • 一个正确的 init进程

    Baseimage-docker附带一个init进程/sbin/my_init,可以正确地处理孤立的子进程,并正确响应SIGTERM。这样你的容器就不会充满僵尸进程,并且docker stop可以正常工作。

  • 修复了与Docker的APT不兼容问题:请参阅Docker问题#1024。

  • syslog优化:它运行syslog守护程序,以便重要的系统消息不会丢失。
  • cron守护进程:它运行一个cron守护进程,以便cronjobs工作。
  • SSH服务器:允许您轻松登录容器以检查或管理事物。

对于选择 RHEL、CentOS 分支的 Base Image,提供安全框架 SELinux 的使用、块级存储文件系统 devicemapper 等技术,这些特性是不能和 Ubuntu 分支通用的。另外需要注意的是,使用的操作系统分支不同,其裁剪系统的方法也完全不同,所以大家在选择操作系统时一定要慎重。

配置管理工具

一般学习Docker时,主要用于基于 Dockerfile 来创建 Image 镜像的,考虑到在实际情况中,我们需要配置和维护Docker宿主机,对容器和镜像进行管理,对构建的进行配置。所以需要结合开发团队的现状,选择一款团队熟悉的工具作为通用工具。

配置工具有很多种选择:Chef、Ansible、Salt Stack、Puppet。其中 Ansible 作为后起之秀,具备简单、强大、无代理的特点。在配置管理的使用中体验非常简单易用,推荐大家参考使用。

Host主机系统

主机系统是Docker 后台进程的运行环境。从开发角度来看,它就是一台普通的单机 OS 系统,我们仅部署Docker 后台进程以及集群工具,所以希望 Host 主机系统的开销越小越好。这里推荐给大家的 Host 主机系统是 CoreOS,它是目前开销最小的主机系统。另外,还有红帽的开源 Atomic 主机系统,有基于Fedora、CentOS、RHEL多个版本的分支选择,也是不错的候选对象。

另外一种情况是选择最小安装操作系统,自己定制Host 主机系统。如果你的团队有这个实力,可以考虑自己定制这样的系统。

优雅实践指南

持续集成的构建系统

开发团队把代码提交到 Git 应用仓库的那一刻,我相信所有的开发者都希望有一个系统能帮助他们把这个应用程序部署到应用服务器上,以节省不必要的人工成本。但是,复杂的应用部署场景,让这个想法实现起来并不简单。

首先,我们需要有一个支持 Docker 的构建系统,这里推荐 Jenkins。它的主要特点是项目开源、方便定制、使用简单,完全拥抱docker。Jenkins 支持安装各种第三方插件,从而方便快捷的集成第三方的应用。

通过Jenkins 系统的 Job 触发机制,我们可以方便的创建各种类型的集成 Job 用例,但缺乏统一标准的 Job 用例使用方法,会导致项目 Job 用例使用的混乱,难于管理维护,这也让开发团队无法充分利用好集成系统的优势,当然这也不是我们期望的结果。所以,敏捷实践方法提出了一个可以持续交付的概念 DeploymentPipeline(管道部署)。通过Docker 技术,我们可以很方便的理解并实施这个方法。

Jenkins 的管道部署把部署的流程形象化成为一个长长的管道,每间隔一小段会有一个节点,也就是 Job,完成这个 Job 工作后才可以进入下一个环节。形式如下:

大家看到上图中的每一块面板在引入 Docker 技术之后,就可以使用 Docker 把任务模块化,然后做成有针对性的 Image 用来跑需要的任务。每一个任务 Image 的创建工作又可以在开发者自己的环境中完成,类似的场景可以参考下图:所以,使用 Docker 之后,任务的模块化很自然地被定义出来。通过管道图,可以查看每一步的执行时间。开发者也可以针对任务的需要,为每一个任务定义严格的性能标准,已作为之后测试工作的参考基础。

所以,使用 Docker 之后,任务的模块化很自然地被定义出来。通过管道图,可以查看每一步的执行时间。开发者也可以针对任务的需要,为每一个任务定义严格的性能标准,已作为之后测试工作的参考基础。

最佳的发布环境

应用经过测试,接下来我们需要把它发布到测试环境和生产环境。这个阶段中如何更合理地使用Docker 也是一个难点,开发团队需要考虑如何打造一个可伸缩扩展的分发环境。其实,这个环境就是基于 Docker 的私有云,更进一步我们可能期望的是提供 API 接口的 PaaS 云服务。为了构建此 PaaS 服务,这里推荐Google Kubernetes。

Kubernetes是一个容器集群管理工具,它提出两个概念:
1 Cluster control plane(AKA master),集群控制面板,内部包括多个组件来支持容器集群需要的功能扩展。

2 The Kubernetes Node,计算节点,通过自维护的策略来保证主机上服务的可用性,当集群控制面板发布指令后,也是异步通过 etcd 来存储和发布指令,没有集群控制链路层面的依赖。
通过官方架构设计文档的介绍,可以详细的了解每个组件的设计思想。这是目前业界唯一在生产环境部署经验的基础上推出的开源容器方案,目前是 CNCF 推荐的容器管理系统的行业参考标准。

需要的监控网站

这里介绍使用一个开源项目来监控容器的运行——Weave Scope,操作简单易用,初学者极力推荐。
首先,在宿主主机执行以下命令来安装和启动 Weave Scope:

sudo curl -L git.io/scope -o /usr/local/bin/scope  //如果下载不下来,可以自己手动去下载再传到宿主机上
sudo chmod a+x /usr/local/bin/scope
scope launch

安装启动后,控制台会输出浏览器服务器 IP 地址和端口信息4040,访问这个URL地址就可以实时监控了:

点击对应的容器即可查看容器信息,包括 CPU 占用率,内存占用,端口映射表,开机时间,IP 地址,进程列表,环境变量等等。并且,通过这个监控网站上的界面操作,可以对容器做一些简单操作:停止,重启,attach,exec 等,提升一定的效率。
当然这是一个很简单的 Docker 容器监控方案,虽然使用者可以通过这个监控网站直接操作容器,所以无需登录宿主主机来进行相关操作,完美实现资源隔离。但是权限并没有做到独立,所以选择的时候要根据你的实际情况进行使用。
此外结合Kubernetes的使用,还可以监控整个集群,总之是一个不错的web监控方案。

镜像加速

方式一:
- 注册 DaoCloud https://account.daocloud.io/signup
- 使用加速功能

curl -sSL https://get.daocloud.io/daotools/set_mirror.sh | sh -s http://f1361db2.m.daocloud.io

方式二:修改/etc/sysconfig/docker
vim /etc/sysconfig/docker
--registry-mirror={加速地址} 比如: --registry-mirror=http://f1361db2.m.daocloud.io https://yzcs6yua.mirror.aliyuncs.com
方式三:修改/etc/default/docker
echo "DOCKER_OPTS=\"\$DOCKER_OPTS --registry-mirror=http://f2d6cb40.m.daocloud.io\"" | sudo tee -a /etc/default/docker
配置好后要重启服务,并查看下是否带上了镜像参数,相关命令如下

service docker restart  //重启服务
ps aux | grep docker  // 查看启动参数

小结

Docker优雅实践方案,其实是一套灵活简单的敏捷解决方案,充分体现了DevOps思想。它克服了之前集群工具复杂、难用的困境,使用统一的 Docker 应用容器概念部署软件应用。
通过引入 Docker 技术,开发团队在面对复杂的生产环境中,可以结合自己团队的实际情况,定制出适合自己基础架构的开发测试运维一体化解决方案。

参考文档 :
https://www.jianshu.com/p/1a82f357c364
https://blog.csdn.net/qian1122221/article/details/82260644
http://guide.daocloud.io/dcs/daocloud-services-9152664.html

Docker简明教程(8)

hzqiuxm阅读(157)评论(0)

Docker的私有仓库

私有仓库搭建与入门

创建步骤

  • 下载官方的仓库镜像 docker pull registry
  • 启动Docker Registry容器,默认情况下,会将私有仓库存放于容器内的/var/lib/registry(版本不同可能会有变动,之前是在/tmp/registry)目录下,这样如果容器被删除,则存放于容器中的镜像也会丢失。
    所以一般情况下会指定本地一个目录挂载到容器内的/var/lib/registry下,如下:
docker run -d --name=my_registry -p 7779:5000 -v /ccuse/myregistry/:/var/lib/registry/ --privileged=true registry
  • 查看Docker Registry进程 docker ps

基本操作

  • 查看Registry仓库中现有的镜像
curl -XGET http://192.168.3.112:7779/v2/_catalog

命令的结果将返回一个镜像的清单

  • 将Docker镜像推到Registry中
//给本地镜像打Tag
docker tag mytomcat9 192.168.3.112:7779/mytomcat9test
//推送镜像到Registry中
docker push 192.168.3.112:7779/mytomcat9test
(会出现错误,因为client与Registry交互默认将采用https访问,但我们在安装Registry时并未配置指定相关的key和crt文件,https将无法访问)
//centos在/etc/sysconfig/docker中做配置
ADD_REGISTRY='--add-registry 192.168.3.112:7779’
INSECURE_REGISTRY=‘--insecure-registry 192.168.3.112:7779’
//ubuntu在/etc/docker/default.json中配置:
{ "insecure-registries":["192.168.3.112:7779"] }
//然后重启docker服务:
service docker restart

WEB管理服务搭建

根据上面官方提供的镜像搭建的私服,只有API的操作方式,对于我们管理自己的镜像不直观也不方便,熟悉nexus私服的都知道,管理一些jar依赖时候可以方便的通过web页面的方式进行。还好我们可以通过hyper/docker-registry-web这个镜像来搭建一个web服务来进行私服镜像的管理。
访问docker的官方,可以搜索到,主要的安装步骤如下(具体的说明请参考官方文档说明https://hub.docker.com/r/hyper/docker-registry-web/):

  • 拉取镜像到本地 docker pull hyper/docker-registry-web
  • 启动:
方式一:
//启动私服镜像服务
docker run -d -p 7779:5000 --name registry-srv registry
//启动web服务
docker run -it -p 7780:8080 --name registry-web --link registry-srv -e REGISTRY_URL=http://registry-srv:5000/v2 -e REGISTRY_NAME=localhost:5000 hyper/docker-registry-web 

方式二:
//带身份验证访问
docker run -it -p 7780:8080 --name registry-web --link registry-srv \
           -e REGISTRY_URL=https://registry-srv:5000/v2 \
           -e REGISTRY_TRUST_ANY_SSL=true \
           -e REGISTRY_BASIC_AUTH="YWRtaW46Y2hhbmdlbWU=" \
           -e REGISTRY_NAME=localhost:5000 hyper/docker-registry-web

方式三:           
//使用配置启动
1 新建配置文件
registry:
  # Docker registry url
  url: http://registry-srv:5000/v2
  # Docker registry fqdn
  name: localhost:5000
  # To allow image delete, should be false
  readonly: false
  auth:
    # Disable authentication
    enabled: false
2 启动
docker run -d -p 7779:5000 --name registry-srv registry
docker run -it -p 7780:8080 --name registry-web --link registry-srv -v $(pwd)/config.yml:/conf/config.yml:ro hyper/docker-registry-web

访问web服务查看仓库中镜像:

为web服务增加删除功能

我们可以看到上面私服中只有查看镜像的操作,没有删除的操作,如果要删除的话,要创建一个给registry用的config.yml,在里面设置可以delete,例如:

``` registry的配置文件
version: 0.1
log:
level: info
formatter: text
fields:
service: registry-srv
environment: production
storage:
cache:
layerinfo: inmemory
filesystem:
rootdirectory: /var/lib/registry
delete:
# 要在ui 上能够删除镜像,enable 的值必须是true
enabled: true
http:
addr: :5000

<pre class="line-numbers prism-highlight" data-start="1"><code class="language-null"><br />还要给web服务也提供一个配置文件,是其能够在页面上显示delete操作的按钮
``` web的配置文件
registry:
# Docker registry url
url: http://registry-srv:5000/v2
# Docker registry fqdn
name: localhost:5000
# To allow image delete, should be false
readonly: false
delete:
enabled:true
auth:
# Disable authentication
enabled: false

它们的启动命令如下:

docker run -d -p 7779:5000 --name registry-srv  -v /ccuse/myregistry/:/var/lib/registry  -v $(pwd)/config-svr.yml:/conf/config.yml  registry
docker run -it -p 7780:8080 --name registry-web --link registry-srv -v $(pwd)/config.yml:/conf/config.yml:ro hyper/docker-registry-web

再次进入私服的web页面查看容器镜像,发现页面上多了delete的操作按钮,我们可以通过这里对镜像进行删除了。当然你也可以进入到容器的保存目录,手动去删除。

Docker简明教程(7)

hzqiuxm阅读(149)评论(0)

Dockerfile入门与实践技巧

在上一节中演示了一个构建静态网站的例子,里面涉及到了Dockerfile的编写,刚接触的同学可能不太理解,所以专门新增一节介绍下Dockerfile的基础知识和一些编写原则与技巧。
然后以构建一个自己的java+tomcat的镜像为例子

理论部分

Dockerfile是用来构建Docker镜像的构建文件,是由一系列命令和参数构成的脚本。
熟悉linux下Shell脚本同学学起来应该是毫不费力,没有经验的掌握好常用的指令,多看看官网现成的例子也应该很快就上手的。

基本语法讲解

1:每条指令都必须为大写字母,且后面要跟随至少一个参数
2:指令按照从上到下,顺序执行
3:#表示注释
4:每条指令都会创建一个新的镜像层,并对镜像进行提交
5:Docker执行Dockerfile的大致流程
(1)docker从基础镜像运行一个容器
(2)执行一条指令,对容器作出修改
(3)执行类似docker commit的操作,提交一个新的镜像层
(4)docker再基于刚提交的镜像运行一个新容器
(5)执行dockerfile中的下一条指令,直到所有指令都执行完成
6:Docker会将构建镜像的过程缓存起来,如果不需要缓存,可以在docker build的时候指定 --no-cache

指令功能介绍

FROM

指定一个已经存在的镜像,也是构建的基础镜像,Dockerfile的第一条必须是FROM

MAINTAINER

设置作者,联系邮件

RUN

指定要运行的命令,建议使用数组的格式,也是exec的格式,如:
RUN[“apt-get”,”install”,”-y”,”nginx”]

EXPOSE

向容器外部公开容器内的端口

WORKDIR

指定在创建容器的时候,在容器内部设置一个工作目录,entroypoint和CMD指定的
程序会在这个目录执行
可以在docker run中使用-w来覆盖工作目录

USER

指定该镜像以什么样的用户去执行,可以单独指定用户,也可以指定用户和组,格
式:USER uid:gid,可以在docker run中通过-u来覆盖,如果都不指定,默认是root

CMD

指定一个容器启动时要运行的命令,如果指定了多条CMD,只有最后一条会执行
例如:CMD[“/bin/bash”,”-l”]
如果在docker run 后面跟上要执行的命令,会覆盖Dockerfile里面的cmd指定的命令

ENTROYPOINT

也用来指定一个容器启动时要运行命令
1:但是它不会被docker run后面的命令覆盖,而是把docker run指定的任何参数当作参数传
递给entroypoint
2:可以和CMD一起用,比如:

ENTROYPOINT[“/usr/sbin/nginx”]
CMD[“-h”]

这样如果docker run的时候不覆盖CMD,那么就是按照
/usr/sbin/nginx –h来运行
如果运行的时候:docker run –it 容器id –g“daemon off;”
那么实际运行就是:
/usr/sbin/nginx –g “daemon off;”
3:如果非要覆盖entrypoint,可以在docker run的时候,设置 --entroypoint 标志

ENV

用来在构建镜像过程中设置环境变量,例如:
ENV MY_PATH /usr/my
这个环境变量可以在后续的任何RUN指令中使用,这就如同在命令前面指定了环境变
量前缀一样;也可以在其它指令中直接使用这些环境变量,比如:
WORKDIR $MY_PATH
可以在docker run 命令中使用–e来指定环境变量,这些变量只在运行时有效
小技巧:
可以在不需要构建缓存的前面,添加一个ENV语句,这样,要后面更新的时候,就修
改一下这个ENV的值

ADD

用来将构建环境下的文件或目录复制到镜像中。
1:只能操作构建环境相对的文件或目录,文件源也可以使用URL格式
2:如果将一个归档文件指定为源文件,docker会自动解压
3:如果目的位置不存在的话,Docker会自动创建全路径
注意:ADD会使得构建缓存无效,ADD后续指令都不能使用之前的构建缓存了

COPY

类似于ADD,COPY只做构建上下文中复制文件,而不会去做文件提取和解压的工作
如果源文件是目录,那么这整个目录会被复制到容器中

VOLUME

用来向镜像创建的容器添加卷,一个卷是可以存在于一个或者多个容器内的特定目
录,这个目录可以绕过联合文件系统,并提供如下共享数据或者对数据进行持久化:
1:卷可以在容器间共享和重用
2:对卷的修改是立即生效的
3:对卷的修改不影响镜像
卷可以让我们把数据、数据库或其它内容添加到镜像中,而不是将这些内容提交到
镜像中,并且允许我们在多个容器间共享这些内容。
注意:如果删除了最后一个使用卷的容器,内部卷就不见了。
可在docker run的时候,使用-v来把宿主机的目录映射到容器,这样数据就能一直保存了

ONBUILD

指定当镜像做为其它镜像的基础镜像时,该镜像触发执行的功能。
ONBUILD在子镜像build的时候,在FROM之后就先执行,并且只能被执行一次不会被孙镜像继承

Dockerfile编写原则

  • 容器应该是短暂的
  • 避免安装不必要的包
  • 每个容器应该只有一个关注点
  • 最小化层的数量
  • 对多行参数进行排序
  • 缓存中间构建的镜像

实践部分

编写一个Dockerfile文件

FROM ubuntu
MAINTAINER hzqiuxm
#把java与tomcat添加到容器中
ADD jdk-8u111-linux-x64.tar.gz /root/src/soft/
ADD apache-tomcat-9.0.0.M26.tar.gz /root/src/soft/
#配置java与tomcat环境变量
ENV JAVA_HOME /root/src/soft/jdk1.8.0_111
ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
ENV CATALINA_HOME /root/src/soft/apache-tomcat-9.0.0.M26
ENV CATALINA_BASE /root/src/soft/apache-tomcat-9.0.0.M26
ENV PATH $PATH:$JAVA_HOME/bin:$CATALINA_HOME/lib:$CATALINA_HOME/bin
#容器运行时监听的端口
EXPOSE 8080
#启动时运行tomcat
# CMD ["/root/src/soft/apache-tomcat-9.0.0.M26/bin/catalina.sh","run"]
# ENTRYPOINT ["/root/src/soft/apache-tomcat-9.0.0.M26/bin/startup.sh" ]
CMD /root/src/soft/apache-tomcat-9.0.0.M26/bin/startup.sh && tail -F /root/src/soft/apache-tomcat-9.0.0.M26/bin/logs/catalina.out

使用Dockerfile来制作镜像
在/root/src/soft/目录下准备以下文件:1 刚创建的Dockerfile文件 2 jdk压缩包 3 tomcat压缩包,然后执行以下命令

docker build -t mytomcat9 .

构建镜像过程日志如下:

[root@localhost docker]# docker build -t mytomcat9 .
Sending build context to Docker daemon 190.8 MB
Step 1/11 : FROM ubuntu
 ---> 16508e5c265d
Step 2/11 : MAINTAINER hzqiuxm
 ---> Using cache
 ---> f5d67d9ca44a
Step 3/11 : ADD jdk-8u111-linux-x64.tar.gz /root/src/soft/
 ---> 34a20e7958ba
Removing intermediate container 8b7d6c0735ca
Step 4/11 : ADD apache-tomcat-9.0.0.M26.tar.gz /root/src/soft/
 ---> b4ce4316740b
Removing intermediate container b208617a6a63
Step 5/11 : ENV JAVA_HOME /root/src/soft/jdk1.8.0_111
 ---> Running in 1ae8272ac0ef
 ---> 52cc51e6fdcd
Removing intermediate container 1ae8272ac0ef
Step 6/11 : ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
 ---> Running in 91ace6986fed
 ---> a4f53b5778a4
Removing intermediate container 91ace6986fed
Step 7/11 : ENV CATALINA_HOME /root/src/soft/apache-tomcat-9.0.0.M26
 ---> Running in caf3441b162c
 ---> d1595d6ae6e7
Removing intermediate container caf3441b162c
Step 8/11 : ENV CATALINA_BASE /root/src/soft/apache-tomcat-9.0.0.M26
 ---> Running in 6e359bd47512
 ---> 23b7222d20fc
Removing intermediate container 6e359bd47512
Step 9/11 : ENV PATH $PATH:$JAVA_HOME/bin:$CATALINA_HOME/lib:$CATALINA_HOME/bin
 ---> Running in 438a22f75079
 ---> 9c0df759c3ed
Removing intermediate container 438a22f75079
Step 10/11 : EXPOSE 8080
 ---> Running in 994985ed6196
 ---> 2bfbb442a498
Removing intermediate container 994985ed6196
Step 11/11 : CMD /root/src/soft/apache-tomcat-9.0.0.M26/bin/startup.sh && tail -F /root/src/soft/apache-tomcat-9.0.0.M26/bin/logs/catalina.out
 ---> Running in 18b045c67d53
 ---> 41bd1cf525b6
Removing intermediate container 18b045c67d53
Successfully built 41bd1cf525b6

启动容器进行测试访问

docker run -i -t -d -p 9080:8080 --name myt9 -v /ccdockermake/tomcat9/test:/root/src/soft/apache-tomcat-9.0.0.M26/webapps/test -v /tomcat9logs/:/root/src/soft/apache-tomcat-9.0.0.M26/logs --privileged=true mytomcat9

  • 可以进入运行的容器进行相关的操作
docker exec -it 容器id /bin/bash
  • 访问容器的tomcat首页
//我是在虚拟机中运行的,所以ip地址是虚拟机的地址
http://192.168.52.128:9080/

Springboot进阶教程系列之

hzqiuxm阅读(454)评论(0)

Springboot中的MonogoDB多文档事务

前言

NOSQL事务之殇

说到关系数据库的事务,开发同学们一定耳熟能详,原子性、隔离性、一致性、持久性是不是如数家珍?熟悉Spring框架的还深谙在Spring中如何优雅的控制事务。但是我们知道Spring本身没有实现事务,其事务控制也是以来底层关系型数据库的。

对使用MongoDB的同学来说,当然不能使用了。因为在MongoDB里单文档的操作是具备原子性的,多文档就不支持了,所在设计MongoDB数据库的时候,就会用到嵌入和引用来规避事务控制,如果向银行转账之类的操作,无法完全规避的情况下,也是通过自己创建一个容器文档,来做仿真事务的。但是在MongoDB4.0中,支持了我们梦寐以求的多文档事务。

官方消息解读

来看下官方文档中的介绍:

Best way to work with data: adding multi-document ACID transactions, data type conversions, native visualizations with MongoDB Charts, the MongoDB Compass aggregation pipeline builder, and the MongoDB Stitch serverless platform.

Intelligently place data where you need it to be: with 40% faster shard migrations, non-blocking secondary replica reads, SHA-2 authentication, and the new MongoDB Mobile database.

Freedom to run anywhere: bringing global clusters and enterprise security with HIPAA compliance to the MongoDB Atlas database service, along with the free community monitoring service, and Kubernetes integration.

我们可以看到4.0中优化了不少特性,但今天只要关注第一条的开头一句就够了: adding multi-document ACID transactions(增加了多文档ACID事务),接下来我们可以喜大普奔了。

MongoDB新版本提供了面向复制集的多文档事务特性。其能满足在多个操作,文档,集合,数据库之间的事务性,事务的特性:一个事务中的若干个操作要么全部完成,要么全部回滚,操作的原子性A,数据更新的一致性C。事务提交时,所有数据更改都会被永久保存D。事务提交前其数据更改不会被外部获取到。还有个重磅消息:多文档事务在4.0版本仅支持复制(副本)集,对分片集群的事务性支持计划在4.2版本中实现。

不过有个前提,事务的ACID操作必须是针对一个已经存在的集合进行,详细的方法和介绍大家可以去读读官方的文档。我接下来要介绍的是我们如何在Springboot中实现MongoDB的多文档事务操作。

阅读下面的内容需要你具备以下知识:对Springboot有一定的使用经验,了解MongoDB的基本概念掌握基本使用。

环境准备

MongoDB环境准备

  • 版本检查:版本必须是4.0+的,如果不是新安装而是老版本升级的,要检查下自己数据库featureCompatibilityVersion参数是不是4.0
db.adminCommand( { getParameter: 1, featureCompatibilityVersion: 1 } )

如果不是的话设置下:db.adminCommand( { setFeatureCompatibilityVersion: "4.0" } )

  • 副本集群搭建:因为多文档事务前提就是需要多个副本集,所以我们至少准备2个副本集,我这里准备的是在内网环境中的一台机器上采用不同的端口实现多副本集,生产环境下一般都会分开。

主副本:192.168.3.112:27117,辅助副本:192.168.3.112:27217

其中一个的配置文件如下:

port = 27117  端口,另一个是27217
dbpath =../dbs/   存放数据文件,相同机器时要不同
logpath=../logs/mongdb.log  存放日志文件, 相同机器时要不同
logappend=true
fork=true
bind_ip=0.0.0.0
replSet = myrepl   集群名称,自己定义

副本集初始化命令:

rs.initiate({_id:"myrepl",members:[
{_id:0,host:'192.168.3.112:27117'},
{_id:1,host:'192.168.3.112:27217'}]
})

注意:这里的IP不是本机的话,一定要填写内网具体的地址,不能用回写地址127.0.0.1;因为这个IP是要和客户端通信的。

配置成功后可以使用rs.config() 来查看下,如果初始化时候信息填错,可以使用rs.reconfig({...})来重新设置下

{
    "_id" : "myrepl",
    "version" : 2,
    "protocolVersion" : NumberLong(1),
    "writeConcernMajorityJournalDefault" : true,
    "members" : [ 
        {
            "_id" : 0,
            "host" : "192.168.3.112:27117",
            "arbiterOnly" : false,
            "buildIndexes" : true,
            "hidden" : false,
            "priority" : 1.0,
            "tags" : {},
            "slaveDelay" : NumberLong(0),
            "votes" : 1
        }, 
        {
            "_id" : 1,
            "host" : "192.168.3.112:27217",
            "arbiterOnly" : false,
            "buildIndexes" : true,
            "hidden" : false,
            "priority" : 1.0,
            "tags" : {},
            "slaveDelay" : NumberLong(0),
            "votes" : 1
        }
    ],
    "settings" : {
        "chainingAllowed" : true,
        "heartbeatIntervalMillis" : 2000,
        "heartbeatTimeoutSecs" : 10,
        "electionTimeoutMillis" : 10000,
        "catchUpTimeoutMillis" : -1,
        "catchUpTakeoverDelayMillis" : 30000,
        "getLastErrorModes" : {},
        "getLastErrorDefaults" : {
            "w" : 1,
            "wtimeout" : 0
        },
        "replicaSetId" : ObjectId("5b6aca1b7ef20d701c739318")
    }
}

Springboot环境准备

  • 加入依赖:本人比较喜欢使用Gradle来管理包依赖,喜欢Maven的同学可以自己根据其语法配置。
    需要注意的是依赖包的版本是要有严格限制的,因为我们目前使用的是spring-data-mongodb的RC版本,目前稳定版中还不支持,所以drive包必须是3.8的,common包里必须是对应的RC版,我们要把自动依赖的低版本包去除掉。依赖包如下所示:
dependencies {
    compile('org.springframework.boot:spring-boot-starter-data-mongodb'){
        exclude group: 'org.springframework.data', module: 'spring-data-mongodb'
        exclude group: 'org.mongodb', module: 'mongodb-driver'
    }
    compile ('org.springframework.data:spring-data-mongodb:2.1.0.RC1'){
        exclude group: 'org.springframework.data', module: 'spring-data-commons'
    }
    compile ('org.mongodb:mongo-java-driver:3.8.0')
    compile('org.springframework.boot:spring-boot-starter-web')
    compile('org.springframework.data:spring-data-commons:2.1.0.RC1')

数据库查看根据

如果还用客户端直接访问服务端来查看MongoDB集合或文档的话,那效率也太低了,工欲善其事必先利其器嘛。所以免费的工具还是推荐Robo 3T,当然如果你使用的是收费的Studio 3T那真的最好了。

其操作界面如下:

与Springboot结合

用Spring注解方式来控制事务

要使用Spring注解方式控制事务,需要使用使用到MongoDB提供的事务管理类,我们要先配置一个

@Bean
MongoTransactionManager transactionManager(MongoDbFactory dbFactory) {
    return new MongoTransactionManager(dbFactory);
}

编写测试类代码

然后创建好需要测试的控制类,服务类,资源类。篇幅原因就不把各个部分的代码贴上来了,只简单介绍下注意点,源代码请到我的个人仓库查看:https://gitee.com/hzqiuxm/mddemo.git
文档实体类Person.java

@Document
public class Person {
    @Id
    private ObjectId id;
    private String name;
    private int age;
    private List<String> address;
    private Map<String,String> des;
     ......
   }

TestController.java是控制类,里面的方法testAdd()用来测试添加一个文档

@GetMapping("/testAdd")
public void testAdd(){

    Person person = new Person("fengxiaoxiong",29);

    ArrayList<String> strings = new ArrayList<>();
    strings.add("浙江省杭州市西湖区");
    strings.add("浙江省杭州市之江区");
    person.setAddress(strings);

    Map<String,String> descrption = new HashMap<>();
    descrption.put("ext1","my");
    descrption.put("ext2","bear");

    person.setDes(descrption);

    personService.addPerson(person);
}

PersonService 是服务类,自动注入实体操作类,并增加一个addPerson()方法,用来添加一个文档。

@Autowired
private PersonRepository personRepository;
@Transactional
public void addPerson(Person person){
    personRepository.insert(person);
}

我们使用注解@Transctional来控制事务
最后的PersonRespository接口是用来操作数据集集合文档的,继承自MongoDBRepository接口,简单的增删改查方法框架都帮我们生成好了,所以不用增加任何方法。

public interface PersonRepository extends MongoRepository<Person,ObjectId> {
}

创建数据库集合和配置文件

本次用到的数据库名称:sale;集合名称Person
在Springboot工程里application.yml配置文件中加入:

spring:
    data:
      mongodb:
        host: 192.168.3.112  //主副本的地址
        port: 27117          //主副本的端口
        replica-set: myrepl  //集群名称
        database: sale       //数据库名称

注意,这里只要配置主副本的地址和端口就可以了,工程启动的时候我们可以从启动日志看到通过连接主副本后,会解析集群的信息然后分别连接到各个副本

测试

无事务方法中出现异常情况

在服务方法中我们加入一个除0的异常代码

public void addPerson(Person person){
    personRepository.insert(person);
    int a = 5/0;
}

这种情况下,因为没有事务文档是会添加成功的,执行后报java.lang.ArithmeticException: / by zero异常错误,但是我们查看数据库,发现记录被添加了:

/* 6 */
{
    "_id" : ObjectId("5b6bb1a29391ca5a04d13514"),
    "name" : "fengxiaoxiong",
    "age" : 29,
    "address" : [ 
        "浙江省杭州市西湖区", 
        "浙江省杭州市之江区"
    ],
    "des" : {
        "ext2" : "bear",
        "ext1" : "my"
    },
    "_class" : "com.example.md.mddemo.Person"
}

有事务方法中出现异常情况

因为加了事务,所以会避免提交,我们把要把TestController.java中testAdd()代码中插入内容修改下:

Person person = new Person("jinguanghui",31);
ArrayList<String> strings = new ArrayList<>();
strings.add("浙江省杭州市余杭区");
strings.add("浙江省杭州市之江区");
person.setAddress(strings);
Map<String,String> descrption = new HashMap<>();
descrption.put("ext1","so");
descrption.put("ext2","cool");

然后把事务注解加回到服务方法addPerson()上

@Transactional
public void addPerson(Person person){
    personRepository.insert(person);
    int a = 5/0;
}

执行后报java.lang.ArithmeticException: / by zero异常错误,但是我们查看数据库,发现记录未被添加事务控制起作用了

有事务无异常情况

最后我们把产生异常的代码 a=5/0删除掉执行,文档被正常插入了

/* 7 */
{
    "_id" : ObjectId("5b6bb3c49391ca5a788cfcfa"),
    "name" : "jinguanghui",
    "age" : 31,
    "address" : [ 
        "浙江省杭州市余杭区", 
        "浙江省杭州市之江区"
    ],
    "des" : {
        "ext2" : "cool",
        "ext1" : "so"
    },
    "_class" : "com.example.md.mddemo.Person"
}

总结

虽然上面只简单使用了一个除0异常来演示,但是原理上是相同的,我们可以在除0异常后加入其它文档操作,最后效果类似。

通过这个简单示例,我们现在如果要使用MongoDB的多文档事务控制功能,必须要注意下面几点:

  • MongoDB的版本必须是4.0
  • 事务功能必须是在多副本集的情况下才能使用
  • 如果不和其他框架结合使用,直接参考官方文档的显示调用方式
  • 事务控制只能用在已存在的集合中
  • 如果想和Springboot框架结合,借用spring-data-mongodb来简化操作,那目前只能使用RC包,并且要剔除老版本包的依赖,自己添加符合要求的包

相信本次事务功能的更新,对MongoDB数据库模式设计会带来一些变化,嵌入文档,数组结合引用和仿真事务的一些设计会有所变化,一些棘手的问题会在一定程度上得到解决。

最后我们期待分片集群事务早点到来,广大Springboot的使用者们相信更期待spring-data-mongodb稳定版对事务支持早日推出。

JAVA语言系列

hzqiuxm阅读(219)评论(0)

前言

Oracle 官方宣称在每年的三月和九月将分别发布两次 Java 的新版本。这意味着 Java 11 将在五个月内面世。得益于如此频繁的发布速度,只要有新功能便会立即加入到新版的 Java 中,这样一来开发者就可以很快享有 Java 的新功能,而不必再像以前那样等待多年。

Java 10的发布相比于 Java 9 只有短短的六个月,然而我们在 Java 10中看到了大量的新功能。对于即将发布的 Java 11,我们同样期待能够有更多的新功能加入。

如今,这些新功能能够快速在 Java 新版本中发布,那么这些新功能到底有哪些?本文将重点介绍当前正在开发和提及的一些有趣的 Java 新功能。

新特性介绍

局部变量类型推断

Java 10 引入了局部变量类型推断的特性。该特性使用var关键字来定义局部变量,并让编译器根据初始化的方式来确定变量类型。如果你使用过 Java 的 lambda 表达式,那么这个特性对你来说并不会太陌生。

实际上,使用过lambda表达式的你可能已经耳熟能详了:

Function<String, String> foo = (s) -> s.toLowerCase();

上面这段代码你会发现没有必要把 s 定义为 String,它的类型会由编译器自动推断出来。使用 Java 10,你可以编写如下代码:

var list = new ArrayList<String>();

编译器会推断 list 是 ArrayList 类型。使用 var关键字可以帮助你减少一些代码的冗长度,尤其是泛型已经存在于变量初始化或者变量名称中时。它容易获得且易于理解,不必依靠 IDE 来告诉你变量的具体类型。

Java 11会进一步增强这一点,所以var也是 lambda 参数的合法类型,这意味着前面提到的 lambda 也可以写成 (var s) -> s.toLowerCase();。你可能会问为什么可以这么写,实际上,忽略类型的效果是一样的。一个主要的原因是,有一个类型的话意味着你可以注释得更好。

具体哪些情况应该使用var,请阅读:http://openjdk.java.net/projects/amber/LVTIstyle.html

原始字符串

目前另一项增强功能被提出并正在积极研究中,那就是原始字符串文字功能。在原始字符串中,字符串中的每个字符都按原样读取,包括换行符!这个功能对于那些需要大量转义或者跨越多行的字符串来说特别有用。例如,这可以是硬编码的 HTML 或 SQL 查询:

String html = "<html>\n" +
             "  <body>\n" +
             "    <p>Hello World.</p>\n" +
             "  </body>\n" +
             "</html>\n";

有了原始字符串文字特性,以上代码可以写成这样:

String html = `<html>
                <body>
                  <p>Hello World.</p>
                </body>
              </html>
             `;

在新的语法中当,反引号(`)用作原始字符串符号。如果你需要在字符串文本中使用反引号,则只需使用双反引号将字符串包围起来,或者三元、四元反引号也可以,只要开始和结束的反引号数量相同即可。

swich表达式

有关 switch 语句的多项改进正在进行,其中就包括了全开模式匹配。我对 switch 表达式充满期待。如果你曾经写过 switch 语句,那么代码可能是这样的:

int val;
switch (str) {
  case "foo": val = 1; break;
  case "bar": val = 2; break;
  case "baz": val = 3; break;
  default: val = -1;
}

有了 switch 表达式,上面的代码可以简化为:

int val = switch (str) {
  case "foo": break 1;
  case "bar": break 2;
  case "baz": break 3;
  default: break -1;
}

这意味着 break 语句能够获得 switch 表达式的结果值,就像 return 获得方法的返回值一样。在以上例子中,break 是 case 的唯一语句,如果继续简化,一种类似 lambda 语法的写法可以将上述代码改为:

int val = switch (str) {
  case "foo" -> 1;
  case "bar" -> 2;
  case "baz" -> 3;
  default -> -1;
}

如上所述,添加 switch 表达式是模式匹配的一个步骤,它使你不仅能够切换编译时常量,还可以是类型、条件检查等等。

目前 switch 表达式已经实现了,经过测试、代码审查和批准之后,相信将其发布到 JDK 中只是一个时间问题。离 Java 11发布还有足够长的时间,在 Java 11中加入 switch 表达式应该没问题。

其他特性

除了上述提到的语言变化之外,还有很多其他的东西可能在近期或者不远的将来出现在 Java 的新版本中。JEP 的提案越来越多,比如支持枚举泛型类型参数的增强枚举、前文已经提到的模式匹配、动态调用、值类型,等等。

如何将单个文件类作为脚本启动,类似于shell、Perl 或者 Python 脚本在命令行的运行方式一样?Java 后台引擎也进行了很多改进。这包括对 JVM 本身的更改,这样的更改对语言层面不会有影响,例如新的超低延迟 GC,用于与本机代码更好地实现互操作性的 API,改变内部类在 JVM 级别(基于嵌套的访问控制)以及一些其他的改进。

SpringCloud微服务系列(7)

hzqiuxm阅读(392)评论(0)

分布式配置中心Spring Cloud Config

起源

为分布式系统中的基础设施或者微服务提供集中化(统一)的外部配置支持,并实现在线更新。

组成

从之前的示例图上,我们可以看到它也是分为服务端和客户端两部分的,其中服务端就是分布式配置中心,它也是一个独立的微服务应用。用来为客户端提供配置,当然它的配置信息可以是本地的也可以是一个远程的配置仓库(通常我们都会是从一个独立的GIT仓库中获取,你肯定知道GIT是可以做版本管理的)。

需要实现统一管理或在线更新配置的其他微服务,扮演的就是客户端的角色。它们从服务端(配置中心)中获取业务相关的配置内容,会在启动或收到总线通知(下一章节会讲到)的时候去加载配置。

配置与映射

服务端映射关系说明

访问配置信息的URL与配置文件的映射关系有如下几种:

  • /{application}/{profile}[/{label}]
  • /{application}-{profile}.yml
  • /{label}/{application}-{profile}.yml
  • /{application}-{profile}.properties
  • /{label}/{application}-{profile}.properties

application:配置文件中除去最后环境信息的部分
profile:环境
label:分支信息,默认是master分支

示例说明:config-client-dev.properties配置文件中,config-client是application,dev是profile

实际案例

spring:
  application:
    name: config-server
  cloud:
    config:
      server:
        git:
          #仓库配置
          uri: https://github.com/hzqiuxm/SpringcloudConfig
          #相对搜索位置,可以多个
          search-paths: respo
      #分支
      label: master

server:
  port: 8888

github工程下信息:

使用postman工具进行访问:

输入:localhost:8888/config-client/dev
返回信息:

{
    "name": "config-client",
    "profiles": [
        "dev"
    ],
    "label": null,
    "version": "c6d735d09f5733200a02f82b7f89e536d8842fa5",
    "state": null,
    "propertySources": [
        {
            "name": "https://github.com/hzqiuxm/SpringcloudConfig/respo/config-client-dev.properties",
            "source": {
                "democonfigclient.message": "hello spring io",
                "foo": "foo version 1"
            }
        },
        {
            "name": "https://github.com/hzqiuxm/SpringcloudConfig/respo/config-client-dev.yml",
            "source": {
                "name": "hzqiuxm"
            }
        }
    ]
}

输入:localhost:8888/config-client/dev, 注意最后的dev代表是分支

返回:

{
    "name": "config-client",
    "profiles": [
        "dev"
    ],
    "label": "dev",
    "version": "7df1a8d5c8f0b2e2c64f27284a6e26e4e3615296",
    "state": null,
    "propertySources": [
        {
            "name": "https://github.com/hzqiuxm/SpringcloudConfig/respo/config-client-dev.properties",
            "source": {
                "democonfigclient.message": "hello spring io",
                "foo": "foo version 3"
            }
        },
        {
            "name": "https://github.com/hzqiuxm/SpringcloudConfig/respo/config-client-dev.yml",
            "source": {
                "name": "hzqiuxm"
            }
        }
    ]
}

客户端配置映射

  • 注意客户端的配置文件名:bootstrap.properties(.yml)
  • spring.application.name={application部分}
  • spring.application.config.profile={profile部分}
  • spring.application.config.label={label部分}
  • spring.application.config.uri={配置中心的地址,带端口}

原理

完整基础架构

之前的示例其实还有一个不完整的地方,就是配置中心在获取了GIT仓库中的配置后,会在本地存储一份,这样不用每次都去远程仓库去取了,即便远程仓库暂时无法访问,也可以保证能返回配置信息给客户端。

完整的基础架构示例图如下:

客户端从配置管理中获取配置流程:
1 客户端根据bootstrap.properties文件配置信息,向配置中心请求配置
2 配置中心查找客户端需要配置信息
3 通过git clone 命令将远程git仓库获取的配置下载到本地的git仓库中
4 配置中心创建spring的ApplicationContext实例,从本地获取配置信息给客户端
5 客户端得到配置文件,加载到ApplicationContext实例中(该配置优先级高于jar包内部的配置)

Git仓库配置

  • 占位符配置: 在配置文件中使用{applicaion}方式配置,服务端会根据客户端的spring.application.name来填充(类似的还有其他几种占位符)

    最佳实践:代码库和配置库名称统一,比如代码库为user-service的微服务的配置库就是user-service-config,这样配置的时候就可以使用占位符{application}来实现通用的配置了

  • 多仓库配置:除了使用application和profile模式陪陪配置仓库外,还可以使用通配符的模式,在配置文件中配置多个仓库来应对复杂的配置需求(建议不要太复杂,维护困难)

  • 指定本地仓库位置:spring.cloud.config.server.git.basedir来配置一个我们准备好的目录
  • 属性覆盖:实现服务端和客户端共同配置,客户端也可以覆盖。通过spring.cloud.config.server.overrides属性来设置键值对参数
  • 安全保护:结合SpringSerurity实现用户名密码访问

加密解密

为了保证数据库账户密码的安全性,不能把明文暴露给开发人员,所以需要对密码进行加密

配置成:{cipher}xxxxxxxxxxxxxxxxxx 的值时,因为{cipher}前缀的作用,配置中心会自动为该值进行解密

准备阶段
  • 去除密钥长度限制:因为需要用到AES加密,而JDK自带的算法密钥长度是有限制,我们要先下一个授权文件,去除长度限制
    > JCE下载:https://www.oracle.com/search/results?Nty=1&Ntk=S3&Ntt=JCE 根据自己的JDK版本选择对应版本解压的授权文件:local_policy.jarUS_export_policy.jar复制到$JAVA_HOME/jre/lib/security目录下,覆盖掉原来内容即可
  • 通过新端点验证:GET访问/encrypt/status 端点,提示你 “NO_KEY”就说明成功了
  • 配置密钥:在配置文件增加 encrypt.key=xxxx xxx是你设置的密钥(更好的方式是使用环境变量ENCRYPT_KEY进行外部存储配置)
  • 使用:使用/encrypt/decrypt端点对请求的body内容进行加解密,它们是post请求

默认采用的是AES对称加密算法,当然也可以采用非对称加密来实现,这就需要我们自己生成密钥对,配置中加入密钥对的位置信息

高可用配置

传统方式

不需要为服务端做任何的额外的配置,所有的服务端都指向一个GIT仓库,通过nginx等其它软件或硬件来实现负载均衡

微服务模式

将服务端作为微服务注册到注册中心,这样客户端调用的时候就可以去注册中心查询,再通过声明式调用或者Riboon来实现客户端的负载均衡

增加配置,注册到注册中心:

eureka:
  client:
    service-url:
      #注册中心地址
      defaultZone: http://localhost:8761/eureka/

客户端相关说明

URL指定配置中心

  • 只有我们配置了spring.cloud.config.uri的时候,客户端才会尝试去连接服务端来获取远程配置信息
  • 配置必须是在bootstrap.properties文件中

失败快速响应与重试

如果你想客户端优先判断能否从服务端获取配置,并快速响应失败的内容,增加下面参数配置:

spring.cloud.config.failFast=ture;

如果只是因为网络波动导致的失败,那代价未免大了些,所以我们希望最好能够自动重试,避免是网络波动原因造成的偶发失败。

开启自动重试需要依赖spring-retry和spring-boot-starter-aop,当然上面失败快速响应的配置也是要的。无需其他额外的配置,这样就实现了失败的自动重试(默认会重试6次,你可以对最大重试次数和重试间隔进行配置)

动态刷新配置

有时候我们需要对配置内容做一些实时更新,这个时候需要引入spring-boot-starter-actuator监控模块,我们主要是用该模块的/refresh端点进行实现的,该端点将用于实现客户端应用配置信息的重新获取与刷新。

熟悉Git仓库的人可能就想到了,我们能够和Web Hook进行关联,这样Git配置变化时,就主动给配置主机发送/refresh请求,实现配置的自动更新(不过维护刷新清单是件烦人的事情,最好的解决方案是通过消息总线)。

简单示例

配置中心(服务端)端口:8888
客户端端口:8881
注册中心端口:8761

远程GIT仓库配置:
master分支下的dev配置文件内容

客户端访问主要代码,简单的获取一个参数值:

@EnableDiscoveryClient
@SpringBootApplication
@RestController
@RefreshScope
public class ConfigClientApplication {

   public static void main(String[] args) {
      SpringApplication.run(ConfigClientApplication.class, args);
   }

   @Value("${foo}")
   String foo;


   @RequestMapping(value = "/hi")
   public String hi(){
      return foo;
   }
}

访问:localhost:8881/hi
返回响应: foo version 1

修改远程仓库内容的值:

发送POST请求: localhost:8881/refresh
返回响应:

[
    "config.client.version",
    "foo"
]

如果响应不成功,提示没有权限的话,在配置中增加:
management.security.enabled = flase,其默认是true

重新访问:localhost:8881/hi
返回响应: foo version 1 add something

SpringCloud微服务系列(6)

hzqiuxm阅读(1028)评论(0)

API网关Zuul

根据我们前几章介绍的SpringCloud微服务组件已经可以搭建一个功能比较完善的服务架构了,如下图所示:

使用Eureka的集群实现高可用的服务注册中心,各服务间的负载均衡使用Ribbon或Feign来包装实现,
对于依赖的服务调用使用Hystrix来进行包装,实现线程隔离并加入熔断机制,避免服务的雪崩效应。
微服务A可以看做是对外的服务,通常也称之为边缘服务,它的负载一般是通过软负载或者硬负载来实现。

看起来一切都似乎很不错,而且我们可以保证,上面的系统架构的确是没有问题的。但是是否存在不完美的地方呢?
我们可以从二个角度来看看:

  • 开发人员:一般情况下,对外的服务部分应该需要考虑一些安全性。所以我们肯定会在A服务访问中加入一些权限的校验机制,比如校验用户登录状态,token令牌等。要注意的是我们采用的是分布式部署,A服务背后依赖的所有服务,我们可能都要加入这些校验逻辑,假设新增一个C服务,那么A服务就要改造,下次新增一个D服务,A服务又要改造;而且A服务的改造影响范围是巨大的,完全背离了设计准则中的开闭原则。还有一个是随着服务的增加,A服务的校验逻辑也将越来越复杂,不仅增加了开发难度,每次测试工作量也越来越大。

  • 运维人员:我们可以看到A服务的负载是由nginx这类软负载或者F5这类硬负载实现的,运维人员必须人工的维护这种路由规则对应的服务实例列表。当有实例发生增减或者IP地址(使用域名可以避免这个问题)变动的时候,也需要手工去同步修改这些信息,保证实例信息与中间件配置的一致性。当系统规模不大时,这个工作量可能还好,一旦系统规模到达3位数,人工维护将变得非常困难,很容易出错。

所以为了解决上面的问题,使得我们的微服务架构更加完美,就需要引入API网关Zuul。

基本介绍

API网关类似设计模式中的Facade(门面)模式,所有的外部客户端访问都需要经过它来进行调度和过滤。它负责实现请求路由、负载均衡、校验过滤、请求转发熔断机制、服务聚合等功能。在SpringCloud的微服务中,API网关的解决方案是Zuul。它作为一个服务注册到Eureka注册中心中,这样就可以获取注册中心的其他微服务实例,从未实现了对路由规则与服务实例维护问题;Zuul提供了一套过滤器的机制,开发者可以利用过滤器机制做一些过滤,拦截和校验工作,大大降低了开发难度。

快速入门演示

我们接下来会搭建一个简单的网关应用,架构中会用到我们之前的一些项目模块,我们知道在springCloud微服务系统中,客户端是既可以作为消费者又可以作为生产者的。所以大家不用关心各个功能间的调用关系,只要把它们看成都是网关服务的提供者就可以了。整体架构如下:

  • 构建网关Zuul
    第一步:引入依赖
compile('org.springframework.cloud:spring-cloud-starter-zuul')

我们观察下这个依赖包装了哪些依赖:

我们可以发现,SpringCloud的zuul是对Netflix-zuul做了封装,还加入了负载均衡Ribbon,熔断保护Hystrix,springboot中的端点管理actuator。

第二步:开启注解,使用@EnableZuulProxy开启Zuul的API网关功能

@EnableZuulProxy
@EnableEurekaClient
@SpringBootApplication
public class ServiceZuulApplication {

   public static void main(String[] args) {
      SpringApplication.run(ServiceZuulApplication.class, args);
   }
}

第三步:增加配置

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
server:
  port: 8769
spring:
  application:
    name: service-zuul
zuul:
  routes:
    api-a:
      path: /api-a/**
      serviceId: service-ribbon
    api-b:
      path: /api-b/**
      serviceId: service-feign

配置的时候我们可以使用url的方式,具体指定某个URL地址对应的实例,但是不推荐,我们应该采用面向服务的路由配置方式,通过serviceId配置服务。

注意:采用URL配置实例的方式是不会使用HystrixCommand进行包装的,所以就丧失了线程隔离和断路器保护,负载均衡的能力。

  • 请求过滤:之前说过,网关是要负责权限校验等操作的,这里为了演示方便就做了简单的token验证
@Component
public class MyFilter extends ZuulFilter{

    private static Logger log = LoggerFactory.getLogger(MyFilter.class);

    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        log.info(String.format("%s >>> %s", request.getMethod(), request.getRequestURL().toString()));
        Object accessToken = request.getParameter("token");
        if(accessToken == null) {
            log.warn("token is empty");
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(401);
            try {
                ctx.getResponse().getWriter().write("token is empty");
            }catch (Exception e){}

            return null;
        }
        log.info("ok");
        return null;

    }
}

关于过滤器的知识,在文章后面会有详解,这里大家只要知道我们可以通过继承ZuulFilter来实现自定义的过滤器。四个方法定义如下:

  • filterType:过滤器类型,决定过滤器在请求的哪个生命周期执行,pre代表在路由之前
  • filterOrder:过滤器执行顺序,根据其返回值决定执行顺序
  • shouldFilter:是否需要被执行
  • run:过滤器的具体执行逻辑

启动各个服务:所有服务清单如下

注意:8763服务是包含了随机睡眠时间的,所以调用的时候会超时报错

  • 输入测试

通过Ribbon访问服务的URL:localhost:8769/api-a/hi?name=hzqiuxm&token=qiuxm
通过Feign访问服务的URL:localhost:8769/api-b/hi?name=hzqiuxm&token=qiuxm

上面两个URL可能返回的结果:

hi hzqiuxm, i am from port8762
{
    "timestamp": 1525409450143,
    "status": 500,
    "error": "Internal Server Error",
    "exception": "com.netflix.zuul.exception.ZuulException",
    "message": "GENERAL"
}
hi hzqiuxm, i am from port8763
hi hzqiuxm, i am from port8762

当URL中不包含token的时候,不会发生调用,网关就直接返回:token is empty
通过简单的示例,我们现在可以理解API网关带来的好处:
1 作为系统统一入口,屏蔽了系统内部各个微服务的调用细节
2 可以与服务治理框架结合,可以实现自动化的实例维护、负载均衡、路由转发。
3 可以实现权限校验与业务逻辑的解耦
4 保证了微服务无状态,易于扩展和测试
5 通过配置实现路由规则,易于测试维护

路由详解

在上面的简单示例中提及,我们要以面向服务方式来配置理由规则。接下来就讲讲路由配置的一些匹配规则个配置要点。

服务路由配置

对于面向服务的路由配置,我们可以采用上面配置中path和serviceId映射配置方式外,还可以采用一种更简洁的方式:zuul.routes.= 比如上面例子中ribbon的配置就等价于:

zuul:
  routes:
    service-ribbon: /api-a/**

相信大家都有一个疑问,我们只配置了服务名称,那zuul网关接收到外部请求,是如何解析并转发到服务的具体实例上的呢?其实在前面已经埋下了伏笔,我们引入依赖的时候特地观察了下zuul所封装的依赖,里面就包含了ribbon。zuul会从注册中心获取所有服务以及它们的实例清单,所以在Eureka的帮助下,网关本身就已经维护了serviceId与实例地址的映射关系。它会根据Ribbon负载策略选择一个具体的实例进行转发,从而完成路由的工作。

服务路由的默认规则

在Zuul构建网关中引入Eureka后,它会为每个服务都自动创建一个默认的路由规则,这些路由规则的path就是使用serviceId配置的服务名作为请求前缀的。
例如:zuul.routes.service-ribbon.serviceId=user-service服务的默认路由规则就是zuul.routes.service-ribbin.path=/user-serviceId/**,所以当你的服务中心有一个service-ribbon服务的时候。上面这两个配置映射就是默认存在的。

默认有时候是比较方便,但是如果我们不希望zuul自动创建类似上面的映射关系的话,我们可以使用zuul.ignored-services参数来设置一个服务名表达式来定义不进行自动创建。
比如:zuul.ignored-services =* 禁止对所有服务自动创建路由规则。

自定义路由映射规则

通过默认规则我们知道,zuul默认生成的path和以serviceId为前缀的,但是如果我们的服务类似带有版本号的,比如:userservice-v1,如果是默认的,那么对应的path就是/userservuce-v1,熟悉REST风格的开发人员知道,一般带有版本号的url是这样的:/v1/userservice/,如果要满足这个需求,我们就要自定义路由映射规则了。步骤也很简单,只要网关程序中增加一个Bean即可:

@Bean
public PatternServiceRouteMapper serviceRouteMapper(){

   return new PatternServiceRouteMapper(
         "(?<name>^.+)-(?<version>v.+$)",
         "${version}/${name}");
}

PatternServiceRouteMapper对象可以通过正则表达式方式来自定义服务与路由映射关系,当匹配不上时,还是会使用默认的路由规则的。

路径匹配

路由匹配路径的表达式,我们可以采用通配符的方式:
? :匹配任意单个字符
* :匹配任意数量的字符
** : 匹配任意数量的字符,支持多级目录

那它们的优先顺序是怎么保证的呢?路由的规则加载算法是通过LinkedHashMap来保存的,说明规则保存是有序的,但是内容的加载是通过线性遍历配置文件来依次加入的。
所以我们要注意properties的配置内容无法保证有序,YAML的文件可以保证有序。

除此之外我们还可以通过zuul.ignored-patterns设置让API网关忽略的URL表达式;可以通过zuul.prefix全局为路由增加前缀信息;使用forward来进行本地跳转

Cookies与头信息

在默认情况下,Zuul会过滤掉HTTP请求头中的一些敏感信息,默认的敏感头信息通过zuul.sensitiveHeaders参数定义。cookies默认在网关中是不会传递的,所以当我们使用了Spring Security、Shiro等安全框架构建路由时,由于cookies无法传递,web应用将无法鉴权。我们有二种方式可以解决:

  • 全面覆盖:设置一个空值,覆盖掉原来的默认值(不推荐)
zuul.sensitiveHeaders=
  • 指定路由的参数配置:方式有二种(推荐)
zuul.routes.<router>.customSensitiveHeaders=true  //对指定的路由开启自定义敏感头
或者
zuul.routes.<router>.sensitiveHeaders=     //将指定路由的敏感头设置为空

断路保护与负载

在上面我们就提到,采用URL配置实例的方式是不会使用HystrixCommand进行包装的,所以就丧失了线程隔离和断路器保护,负载均衡的能力。所以我们应该都采用面向服务的方式来进行配置。我们也可以通过Hystrix和Ribbon的参数来调整路由请求的各种超时时间。

hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds    //执行超时时间
ribbon.ConnectTimeout   //路由转发请求时,创建请求连接的超时时间,应该小于断路器超时时间
ribbon.ReadTimeout      //路由转发请求的超时时间,也应该小于断路器超时时间
zuul.retryable = false            //全局关闭自动重试机制
zuul.routes.<route>.retryable = false    //指定路由关闭自动重试机制

过滤器详解

过滤器简介

过滤器是Zuul中的核心,一个Zuul网关主要就是对请求进行路由和过滤。路由功能负责将外部请求转发到具体的微服务实例上,过滤功能负责对请求的处理过程进行干预,是实现请求校验、服务聚合等功能的基础。实际上路由转发也是通过过滤器来完成的。每一个进入Zuul的HTTP请求都会经过一系列的过滤器处理链得到请求响应并返回客户端的。

Zuul过滤器必须包含4个基本特征:过滤类型、执行顺序、执行条件、具体操作。在之前的示例中我们已经看到过,其实它们就是ZuulFilter抽象类和IZuulFilter接口定义的抽象方法:

boolean shouldFilter();
Object run();
abstract public String filterType();
abstract public int filterOrder();
  • shouldFilrer:是否要执行该过滤器
  • run:过滤器的具体逻辑,我们可以实现自定义的逻辑
  • filterType:过滤器类型

    pre: 在请求被路由之前调用
    routing:在路由请求时调用
    post:在routing和error之后调用
    error:处理请求发生错误时调用

  • filterOrder:过滤器执行顺序,数值越小优先级越高

下面是一个请求在各个过滤器中生命周期:

  • 外部请求过来时,首先进入第一个阶段pre,主要是对路由前的请求做一些前置加工,比如:请求校验。
  • 完成pre类过滤器处理后,请求进入第二个阶段:routing,该阶段是路由转发阶段,将请求转发到具体的服务实例的过程,当服务实例将请求结果返回之后,该阶段完成、
  • 完成routing后,进入第三个阶段:post,该阶段不仅可以获取到请求信息,还可以获取到服务实例的返回信息。所以可以对结果进行加工转换后返回给客户端
  • error是一个特殊阶段,在上述三个阶段中发生异常时就会触发,但是要注意,它最后是流向post阶段的,因为post才能将结果返回给客户端。

核心过滤器

下面对上面提到的几个过滤器,我们详细了解下,有助于我们更好的理解Zuul以及自定义自己业务逻辑。
我们可以前往org.springframework.cloud.netflix.zuul.filters包下查看每种过滤器类型下包含的过滤器。

pre过滤器

  • ServletDetectionFilter,执行顺序-3,是最先被执行的过滤器。用来检测请求是否是通过Spring的DispatcherServlet处理运行的。
    一般情况下,发送到API网关的外部请求都是通过Spring的DispatcherServlet处理的。
    除了通过/zuul/*路径访问的请求,会绕过DispatcherServlet,被ZuulServlet处理,主要是用来处理大文件的上传,这个路径可以通过zuul.servletpath参数进行配置。

  • Servlet30WrapperFilter,执行顺序-2,是第二个执行的过滤器。目前对所有请求生效,主要将原始HttpServletRequest包装成为Servlet30RequestWrapper对象。

  • FormBodyWrapperFilter,执行顺序-1,是第三个执行的过滤器。主要目的讲符合要求的请求体包装成FormBodyRequestWrapper对象。它只对两类请求生效,通过ContentType判断:

    1.application/x-www-from-urlencoded(会将表单内的数据转换为键值对)
    2.multipart/form-data(既可以上传文件,也可以上传键值对,它采用了键值对的方式,所以可以上传多个文件。)且是 DispatcherServlet处理的

  • DebugFilter,执行顺序是1,是第四个执行的过滤器。主要提供debug信息,辅助分析问题。

  • PreDecorationFilter,执行顺序是5,是pre阶段最后执行的过滤器。主要是根据上下文参数对请求进行预处理。
routing过滤器

  • RibbonRoutingFilter,执行顺序10,是route阶段第一个执行的过滤器。只对请求上下文中有serviceId参数的请求进行处理,换句话说就是只对面向服务的配置生效。主要目的是进行请求转发。

  • SimpleHostRoutingFilter,执行顺序100,是route阶段第二个执行的过滤器。只对url配置路由规则生效,一般不建议这么配置,因为不具备线程隔离和断路器功能。

  • SendForwardFilter,执行顺序500,是route阶段第三个执行的过滤器。只处理请求上下文中存在forward.to参数的请求,处理路由规则中forward本地跳转配置。

post过滤器

  • SendErrorFilter,执行顺序0,是post阶段第一个执行的过滤器。需要请求上下中,要有error.status_code参数,所以在自己设置的异常中要设置该参数。主要是用来处理有错误的请求响应。

  • LocationRewriteFilter,执行顺序900。主要目的将location头重写为zuul的URL。

  • SendResponseFilter,执行顺序1000,是post阶段最后一个执行的过滤器。主要目的是利用上下文的响应信息来组织需要发送各个客户端的响应内容。

异常处理

自定义实现继承ZuulFilter类,对自定义过滤器中处理异常的两种解决方法:
1 通过在阶段过滤器中增加try-catch块,实现内部异常处理。注意一定要设置error.status.code参数才会被SendErrorFilter处理,该种方式是对开发人员基本要求。
2 利用自定义的ErrorFilter类处理,利用error类型过滤器的生命周期特点,集中处理其它几个阶段抛出的异常信息。继承ZuulFilter过滤器,指定为error类型,在run()方法中也要设置error.status.code。该种方式作为第一种方式的补充,防止意外情况发生。

error过滤器问题

前面3个阶段出错时,都会走到这个过滤器中。但是最后错误参数起作用的关键是在post阶段的SendErrorFilter过滤器里,所以在error处理之后,还要进入到post的处理阶段才能生效,但是post阶段本身出错后,是不会进入post阶段的。

解决方案:
1 直接实现error过滤器时,组织实现(不推荐,这样错误返回代码会有多处)
2 依然交给SendErrorFilter来处理(继承SendErrorFilter类,复用run方法,重写类型,执行顺序要大于SendErrorFilter,执行条件里只执行post阶段产生的异常即可,要做到判断哪个阶段的异常,需要对过滤器的核心处理器FilterProcessor进行扩展,实现自定义的过滤器处理器,并记录下该信息)

注意:要使扩展的过滤器处理类生效,需要调用FilterProcessor.setProcessor(新的过滤器处理器)方法来启动。

自定义异常信息

默认的错误信息一般并不符合系统设计的响应格式,所以我们要对返回的异常信息进行定制。
自定义异常信息两种方法:
1 编写一个自定义的post过滤器(类似重写SendErrorFilter实现),自己组织响应结果
2 不采用重写方式,可以对/error端点实现,通过自定义实现一个错误属性类覆盖默认的ErrorAttribute.class

下图是对核心过滤器主要信息的一个汇总思维导图:

核心处理器与禁用过滤器

FilterProcessor负责管理和执行各种过滤器,自定义的过滤器处理器,需要设置后才能生效,通过setProcessor()方法来设置。
Zuul.<SimpleClassName过滤器类型>.<FilterType过滤器类型>.disable=true来进行设置禁用某个过滤器
例子:zuul.AccessFilter.pre.disable=true

动态加载

我们可以动态修改路由规则,动态添加和删除过滤器。

动态路由

这个需要依赖下一章节中介绍的配置中心来实现,通过配置中心,类似配置文件动态刷新。

动态过滤器

本身过滤是通过编码实现的,我们可以借助JVM实现的动态语言,比如Groovy来实现。
目前应该还是一个半成品,处理一些简单的过滤功能,应该没有问题,目前还是不要大规模的进行使用。

网关的高可用

网关的高可用一般由二种方式:
一种是网关客户端不注册到注册中心,比较多的服务网关就是直接提供给外部调用的,所以采用这种方式,架构图如下:

一种是网关客户端也注册到注册中心,这样就不需要额外的软负载或者硬负载了,直接都由客户端负载来实现,架构如下:

网关小结

  • Zuul网关路由规则的配置要采用面向服务的配置,学习了配置中心后,我们就可以实现动态路由了
  • 掌握请求在网关中的生命周期
  • 了解Zuul网关中的核心过滤器
  • 掌握自定义各种类型的过滤器满足实际业务场景需求
  • 掌握自定义异常处理的两种方法
  • 掌握自定义异常信息的两种该方法
  • 掌握自定义过滤器处理器定义
  • 掌握网关的高可用架构设置

欢迎加入紫牛小筑

进入小筑关于作者