转载请注明出处:simiam.com
py-awesome-kit是本人最近在折腾的一个python工具箱,它提供了大数据开发与数据分析工作过程中常用的功能,希望后续能慢慢丰富与完善。
项目地址:https://github.com/monkeychen/py-awesome-kit
1
2
3
4
5
6
7
8
9
10
# 备份已安装python库
pip3 freeze > python3-installed-libs.txt
# 删除已安装的python
# Linux依赖包下载地址:https://vault.centos.org/7.3.1611/os/x86_64/Packages/
mkdir /env/python3.7
cd /env/Python-3.7.9
./configure --prefix=/env/python3.7 --enable-shared --enable-optimizations
make&&make install
1
2
3
4
5
6
7
8
9
mkdir jupyterlab
cd jupyterlab
# 下载
pip3 download jupyterlab -i https://pypi.tuna.tsinghua.edu.cn/simple
# 安装
cd ..
pip3 install jupyterlab/jupyterlab-3.0.1-py3-none-any.whl --no-index --find-links=./jupyterlab
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 进入项目根目录
cd <project_root>
# 构建/打包
python3 setup.py bdist_wheel
# 安装至python库
pip install dist/py-awesome-kit-<version>-py3-none-any.whl
# 应用开发过程中会频繁变更,每次安装都需要先卸载旧版本很麻烦。
# 使用 develop 开发模式安装的话,实际代码不会拷贝到 site-packages 下,而是除一个指向当前应用的链接(*.egg-link)。
# 这样当前位置的源码改动就会马上反映到 site-packages。使用如下:
pip install -e . # 或者 python setup.py develop
# 如需卸载,使用如下命令:
pip uninstall fmcc-awesome-kit
查看依赖包: 可以使用yum deplist命令来查找rpm包的依赖列表。例如,要查找git的rpm依赖包:
1
yum deplist git
方案一(推荐):repotrack命令
1
2
3
4
5
# 安装yum-utils
yum -y install yum-utils
# 下载 git 全量依赖包
repotrack git
方案二:yumdownloader命令
1
2
3
4
5
# 安装yum-utils
yum -y install yum-utils
# 下载 git 依赖包
yumdownloader --resolve --destdir=/tmp git
参数说明:
–destdir:指定 rpm 包下载目录(不指定时,默认为当前目录)
–resolve:下载依赖的 rpm 包。
注意:仅会将主软件包和基于你现在的操作系统所缺少的依赖关系包一并下载。
方案三:yum 的downloadonly插件
1
2
# 下载 git 依赖包
yum -y install git --downloadonly --downloaddir=/tmp
注意:与 yumdownloader 命令一样,也是仅会将主软件包和基于你现在的操作系统所缺少的依赖关系包一并下载。
离线安装所有下载的rpm包
1
rpm -Uvh --force --nodeps *.rpm
其他命令
1
2
3
repoquery git
yum provides git
1
2
之前已经是https的链接,现在想要用SSH提交怎么办?
直接修改项目目录下 .git文件夹下的config文件,将地址修改一下就好了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 问题:编写shell脚本后,在bash下单独执行没有问题,但在crontab中无法执行后报如下错误信息:
# [psql: error while loading shared libraries: libpq.so.5: cannot open shared object file: No such file or directory]
# 原因剖析:
# crontab有一个坏毛病,就是它总是不会缺省的从用户profile文件中读取环境变量参数,经常导致在手工执行某个 脚本时是成功的,但是到crontab中试图让它定期执行时就是会出错。
# 解决方案一(推荐):
# ------------------------------
sudo cp ./bin/greenplum-x86_64.conf /etc/ld.so.conf.d/greenplum-x86_64.conf
sudo ldconfig
# 用以下命令检查是否生效
ldconfig -p | grep green
# ------------------------------
# 解决方案二:通过在每个被crontab调用的shell文件前手动设置LD_LIBRARY_PATH环境变量。
export PATH="$PATH:/opt/mssql-tools/bin"
source /env/greenplum/clients/greenplum_clients_path.sh
source /env/greenplum/loaders/greenplum_loaders_path.sh
PYTHONPATH环境变量导致的问题如果系统中未设置PYTHONPATH环境变量,将导致python some_module.py 启动时会无法当前目录添加到sys.path中,
最终导致import项目中其他目录下的模块报错:未发现指定模块。
解决办法如下:
1
2
# vim ~/.bashrc 或 vim ~/.bash_profile 或 sudo vim /etc/profile
export PYTHONPATH=":$PYTHONPATH"
PS:就算已设置PYTHONPATH,如果在crontab中通过shell调用python脚本,仍然会发现当前目录未添加进sys.path中,导致模块加载报错。
原因为:crontab启动时是不会执行~/.bashrc 或 ~/.bash_profile 或 /etc/profile三个文件中的任何一个文件的,
这将导致crontab的执行上下文中并不存在中PYTHONPATH,即本质原因仍然是PYTHONPATH未设置问题。
解决办法多种:
PYTHONPATH。转载请注明出处:simiam.com
Node是JavaScript语言的服务器运行环境。
所谓"运行环境"有两层意思:首先,JavaScript语言通过Node在服务器运行,在这个意义上,Node有点像JavaScript虚拟机;其次,Node提供大量工具库,使得JavaScript语言与操作系统互动(比如读写文件、新建子进程),在这个意义上,Node又是JavaScript的工具库。
Node内部采用Google公司的V8引擎,作为JavaScript语言解释器;通过自行开发的libuv库,调用操作系统资源。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 如macOS下的二进制包
sudo mkdir -p /usr/local/lib/nodejs
sudo cd /usr/local/lib/nodejs
sudo wget https://nodejs.org/dist/v10.15.3/node-v10.15.3-darwin-x64.tar.gz
sudo tar -zxvf node-v10.15.3-darwin-x64.tar.gz
# 设置环境变量,如在~/.profile文件末尾增加如下内容:
export PATH=/usr/local/lib/nodejs/node-v10.15.3-darwin-x64/bin:$PATH
# 执行如下命令使配置生效:
. ~/.profile
# 测试安装是否成功
node -v
npm version
npx -v
1
2
3
4
5
6
7
8
# 通过npm安装n模块
sudo npm install n -g
# 通过n模块,将node.js更新为最新发布的稳定版
sudo n stable
# n模块也可以指定安装特定版本的node
sudo n 10.10.21
demo.js脚本文件,可以这样执行:1
2
3
$ node demo
# 或者
$ node demo.js
-e参数,可以执行代码字符串:1
2
$ node -e 'console.log("Hello World")'
# 执行输出结果为:Hello World
REPL是Node.js与用户互动的shell,各种基本的shell功能都可以在里面使用,比如使用上下方向键遍历曾经使用过的命令。
在命令行键入node命令,后面没有文件名,就进入一个Node.js的REPL环境(Read–eval–print loop,"读取-求值-输出"循环),可以直接运行各种JavaScript命令:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ node
> 1+1
2
>
# 特殊变量下划线(_)表示上一个命令的返回结果
> _ + 1
3
# 在REPL中,如果运行一个表达式,会直接在命令行返回结果。如果运行一条语句,就不会有任何输出,因为语句没有返回值。
# 如下面代码的第二条命令,没有显示任何结果。因为这是一条语句,不是表达式,所以没有返回值
> x = 1
1
> var x = 1
npm是Node的模块管理器,功能极其强大,它是Node获得成功的重要原因之一。npm是随node一起发布安装的,不需要单独安装。正因为有了npm,我们只要使用如下一行命令,就能安装或更新别人写好的模块。
1
2
3
4
# 安装新模块
sudo npm install module_name@version [-g]
# 更新新模块
sudo npm update module_name@version [-g]
参数选项-g代表是全局安装,即会安装到/usr/local/lib/node_modules目录下,如果不加则为本地安装,会将模块安装在当前目录的node_modules目录下。
因为在安装Node的时候,会连带一起安装npm。但是Node附带的npm可能不是最新版本,最好用下面的命令,更新到最新版本。
1
$ npm install npm@latest -g
上面的命令中,@latest表示最新版本,-g表示全局安装。所以,命令的主干是npm install npm,也就是使用npm安装自己。之所以可以这样,是因为npm本身与Node的其他模块没有区别,其本质上也是一个node模块,因此可以通过npm命令来进行版本更新。
然后,运行下面的命令,查看各种信息。
1
2
3
4
5
6
7
8
9
10
11
# 查看 npm 命令列表
$ npm help
# 查看各个命令的简单用法
$ npm -l
# 查看 npm 的版本
$ npm -v
# 查看 npm 的配置
$ npm config list -l
由于众所周知的原因,我们被墙的厉害,所以使用npm下载模块时候会发现效率真的很慢,所以推荐淘宝的NPM镜像。
淘宝的NPM镜像是一个完整
npmjs.org镜像,你可以用此代替官方版本(只读),同步频率目前为10分钟一次以保证尽量与官方服务同步。
使用方式1:在使用npm命令时指定registry参数值为https://registry.npm.taobao.org
1
2
# 通过在使用npm命令时指定`registry`参数值为`https://registry.npm.taobao.org`
npm install <module_name> --registry=https://registry.npm.taobao.org
使用方式2:以linux系统为例,在用户目录或工程项目根目录下创建文件.npmrc,并添加如下内容:
1
registry=https://registry.npm.taobao.org/
registry参数了,因为npm会优先从项目根目录或用户目录下的.npmrc文件中获取registry值。.npmrc文件的registry值可以设置成不同的URL。.npmrc文件的优先级更高。使用方式3:我们可能通过安装cnpm模块来使用淘宝的NPM镜像,安装命令如下:
1
npm install -g cnpm --registry=https://registry.npm.taobao.org
cnpm安装成功以后,就可以直接使用cnpm来安装模块,简单便捷。
在企业内部,为了使前端组件化开发更高效,搭建NPM私有仓库是一个不错的选择。安装教程网上很多,本文不再详情介绍,请参考如下文章:
npm私有仓库安装指南:Nexus上搭建npm本地服务器
如果想在同一台机器,同时安装多个版本的node.js,就需要用到版本管理工具nvm。
1
2
$ git clone https://github.com/creationix/nvm.git ~/.nvm
$ source ~/.nvm/nvm.sh
安装以后,nvm的执行脚本,每次使用前都要激活,建议将其加入~/.bashrc文件(假定使用Bash)。激活后,就可以安装指定版本的Node,命令如下:
1
2
3
4
5
6
7
8
9
10
11
# 安装最新版本
$ nvm install node
# 安装指定版本
$ nvm install 0.12.1
# 使用已安装的最新版本
$ nvm use node
# 使用指定版本的node
$ nvm use 0.12
nvm也允许进入指定版本的REPL环境(Read–eval–print loop,"读取-求值-输出"循环):
REPL是Node.js与用户互动的shell,各种基本的shell功能都可以在里面使用,比如使用上下方向键遍历曾经使用过的命令。
1
$ nvm run 0.12
如果在项目根目录下新建一个.nvmrc文件,将版本号写入其中,就只输入nvm use命令即可,不再需要附加版本号。
下面是其他经常用到的nvm使用命令:
1
2
3
4
5
6
7
8
# 查看本地安装的所有版本
$ nvm ls
# 查看服务器上所有可供安装的版本。
$ nvm ls-remote
# 退出已经激活的nvm,使用deactivate命令。
$ nvm deactivate
转载请注明出处:simiam.com
广告:
转载请注明出处:simiam.com
本文主要是对Maven学习与使用过程中的一些经验的总结,不是Maven的入门教程,如需要系统学习Maven,您可以看一下《Maven实战》这本书,或看下以下几个在线教程:
除了掌握基本内容外,还需要特别关注以下几个内容:
<dependencyManagement>中且类型为pom的依赖项,个人感觉其主要作用在于对依赖版本进行分类管理。构建Maven项目的时候,如果没有进行特殊的配置,Maven会按照标准的目录结构查找和处理各种类型文件。
src/main/java和src/test/java
这两个目录中的所有
*.java文件会分别在comile和test-comiple阶段被编译,编译结果分别放到了target/classes和targe/test-classes目录中,但是这两个目录中的其他文件都会被忽略掉。src/main/resources和src/test/resources
这两个目录中的文件也会分别被复制到
target/classes和target/test-classes目录中。target/classes
打包插件默认会把这个目录中的所有内容打入到
jar包或者war包中。
资源文件是Java代码中要使用的文件。代码在执行的时候会到指定位置去查找这些文件。前面已经说了Maven默认的处理方式,但是有时候我们需要进行自定义的配置,比如下面两种情况:
.java文件一起放在src/main/java目录(如mybatis或hibernate的表映射文件)。classes目录中。这些情况下就需要在pom.xml文件中修改配置了。目前主要有如下两种方法修改配置:
<build>元素下添加<resources>进行配置。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<build>
......
<resources>
<resource>
<directory>src/main/resources</directory>
<excludes>
<exclude>**/*.properties</exclude>
<exclude>**/*.xml</exclude>
</excludes>
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
......
</build>
<build>的<plugins>子元素中配置maven-resources-plugin等处理资源文件的插件。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>2.5</version>
<executions>
<execution>
<id>copy-xmls</id>
<phase>process-sources</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${basedir}/target/classes</outputDirectory>
<resources>
<resource>
<directory>${basedir}/src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
resources元素与maven-resources-plugin插件一般都是搭配在一起使用实现复杂的资源文件处理逻辑。
默认情况下,Maven会从项目的src/main/resources目录下查找资源。如果你的资源不在此目录下,可以用<resources>标签指定,同时也支持多个目录,也支持<include>,<exclude>之类的精细化配置。
1
2
3
4
5
6
7
8
<resources>
<resource>
<directory>src/main/resources1</directory>
</resource>
<resource>
<directory>src/main/resources2</directory>
</resource>
</resources>
有的时候,资源文件中存在变量引用,可以使用<filtering>标签指定是否替换资源中的变量。变量的来源默认为pom文件中的<project>根元素下的<properties>标签或<profile>下的<properties>标签中定义的变量;当然也可以在<build>中定义过滤器资源。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<project>
<properties>
......
</properties>
<build>
<!-- 自定义过滤器资源文件 -->
<filters>
<filter>filter-values.properties</filter>
</filters>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
<profiles>
<profile>
......
</profile>
</profiles>
</project>
如果资源中本来存在不需要被替换的${}字符,则可以在$前加\,同时在maven-resources-plugin插件配置的<configuration>中使用<escapeString>;若目录下存在二进制文件,则需要通过以下的后缀名来排除。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
<configuration>
<!-- 配置资源文件中的转义字符 -->
<escapeString>\</escapeString>
<encoding>UTF-8</encoding>
<!-- 如目录下存在二进制文件,则需要通过以下的后缀名来排除 -->
<nonFilteredFileExtensions>
<nonFilteredFileExtension>pdf</nonFilteredFileExtension>
<nonFilteredFileExtension>swf</nonFilteredFileExtension>
</nonFilteredFileExtensions>
</configuration>
</plugin>
如果你需要在其他阶段拷贝资源文件,可以使用插件目标copy-resources,类似配置如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
<executions>
<execution>
<id>copy-resources</id>
<phase>validate</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${basedir}/target/extra-resources</outputDirectory>
<resources>
<resource>
<directory>src/non-packaged-resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
更详细的配置请参考
maven-resources-plugin插件官网
简单总结:resources及其对应的插件的作用就是用于资源文件的处理(变量替换、编译时资源文件复制等),为后续的项目打包阶段作准备。
一定要注意与
maven-assemble-plugin插件区别开来,因为assemble也涉及到配置文件的拷贝。
本文章所讲的测试主要是指单元测试与集成测试:
maven-surefire-plugin插件执行单元测试maven-failsafe-plugin插件执行集成测试在pom.xml中配置JUnit,TestNG测试框架的依赖,即可自动识别和运行src/test/Java目录下利用该框架编写的测试用例。常用配置如下:
1
2
3
4
5
6
7
8
9
10
11
12
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
<configuration>
<!-- 在构建过程中如需要跳过单元测试阶段,则将skipTests节点值设置为true -->
<skipTests>true</skipTests>
<!-- 忽略测试失败以继续构建项目,不推荐 -->
<testFailureIgnore>true</testFailureIgnore>
</configuration>
</plugin>
想跳过单元测试还可以使用如下方式:
1
2
3
4
5
# 方式一:不执行测试用例,但编译测试用例类生成相应的class文件至target/test-classes下
mvn install -DskipTests
# 方式二:不执行测试用例,也不编译测试用例类
mvn install -Dmaven.test.skip=true
当然,如果你明确用的是JUnit4.7及以上版本,可以明确声明:
1
2
3
4
5
6
7
8
9
10
11
12
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.19</version>
<dependencies>
<dependency>
<groupId>org.apache.maven.surefire</groupId>
<artifactId>surefire-junit47</artifactId>
<version>2.19</version>
</dependency>
</dependencies>
</plugin>
surefire默认的查找测试类的模式如下:
**/Test*.java**/*Test.java**/*TestCase.java如果由于历史原因,测试类不符合默认的三种命名模式,可以通过pom.xml设置maven-surefire-plugin插件添加命名模式或排除一些命名模式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.5</version>
<configuration>
<includes>
<include>**/*Tests.java</include>
</includes>
<excludes>
<exclude>**/*ServiceTest.java</exclude>
<exclude>**/TempDaoTest.java</exclude>
</excludes>
</configuration>
</plugin>
更详细的配置可直接参考surefire插件官网
集成测试过程涉及运行环境准备,如启动tomcat容器等。而单元测试插件有一个特征是如果有一个单元测试用例失败,则会中止整个Maven构建过程,此时会导致之前创建的集成测试环境如tomcat容器未能及时关闭,因此便有了failsafe的用武之地。
Maven构建生命周期中有四个阶段与集成测试有关:
当failsafe插件在构建过程的integration-test或verify阶段被调用而进行集成测试时,测试用例执行过程中就算失败也不会中止构建过程,而会继续执行post-integration-test阶段以进行环境清理操作。
maven-failsafe-plugin插件需要与各种容器插件搭配使用,常用的容器类插件有:tomcat,jetty或者更高级的cargo等。
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
56
57
58
59
60
61
62
63
<plugins>
<!--指定单元测试不执行 否则先执行单元测试 地址不通-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18</version>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<!--集成测试的插件配置-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.19.1</version>
<configuration>
<suiteXmlFiles>
<!--指定配置文件-->
<suiteXmlFile>testng.xml</suiteXmlFile>
</suiteXmlFiles>
</configuration>
<executions>
<execution>
<id>integration-tests</id>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
<!--tomcat插件-->
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<!--部署的端口和配置-->
<configuration>
<port>8080</port>
<path>/</path>
</configuration>
<executions>
<!--什么时候启动 什么时候终止-->
<execution>
<id>tomcat-run</id>
<goals>
<goal>run-war-only</goal>
</goals>
<phase>pre-integration-test</phase>
<configuration>
<fork>true</fork>
</configuration>
</execution>
<execution>
<id>tomcat-shutdown</id>
<goals>
<goal>shutdown</goal>
</goals>
<phase>post-integration-test</phase>
</execution>
</executions>
</plugin>
</plugins>
更详细的配置可直接参考failsafe插件官网
Maven中的打包类型有:jar包、war包以及zip等方便部署的压缩包。
Jar包又可分为普通的jar包、可执行jar包。前者通过maven-jar-plugin插件就能搞定。对于打可执行的jar包有多种方式:
maven-jar-plugin和maven-dependency-plugin插件打包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
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.6</version>
<configuration>
<archive>
<manifest>
<!-- 在MANIFEST.MF加上Class-Path项并配置依赖包 -->
<addClasspath>true</addClasspath>
<!-- 指定依赖包所在目录 -->
<classpathPrefix>lib/</classpathPrefix>
<mainClass>com.simiam.Main</mainClass>
<manifestEntries>
<!-- 将当前目录也加入到classpath中 -->
<Class-Path>./</Class-Path>
</manifestEntries>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.10</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
上述配置所生成的MANIFEST.MF文件中将包含如下内容:
1
2
Class-Path: lib/commons-logging-1.2.jar lib/commons-io-2.4.jar
Main-Class: com.simiam.Main
maven-jar-plugin插件只是生成MANIFEST.MF文件,maven-dependency-plugin插件用于将依赖包拷贝到<outputDirectory>${project.build.directory}/lib</outputDirectory>指定的位置,即target/lib目录下。
这种打包方式可以实现依赖的jar包与自研jar包分开,但配置相关的资源文件也被打进jar包中,后续想修改配置信息将很头痛,同时这种打包方式无法解决部署文件目录结构复杂的场景。
延伸阅读:
configuration下的classifier节点的作用是什么?在以前的插件版本中会存在excludes配置项不生效的问题,目前这个问题已不存在;如果出现这个问题,那一般是默认的执行目标不对导致(mvn package的运行日志中maven-jar-plugin会执行两次,但目标不一样,分别为default-jar, default)。
maven-assembly-plugin插件打包1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.5.5</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
<configuration>
<archive>
<manifest>
<mainClass>com.simiam.Main</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
这种打包方式会将所有依赖的jar包中的内容抽取出来,并与自研的代码合在一起打在同一个Jar包中。不过,如果项目使用了SPI机制,且多个依赖中都有SPI实现类,则用这种方式打出来的包会因为相互覆盖问题导致运行时的不确定性甚至出错。比如项目中用到了Spring Framework,将依赖打到一个jar包中,运行时会出现读取XML schema文件出错。原因是Spring Framework的多个jar包中包含相同的文件spring.handlers和spring.schemas,如果生成一个jar包会互相覆盖。此时可以借助maven-shade-plugin插件来解决这个问题。
使用maven-shade-plugin插件解决Spring应用打包覆盖问题的配置如下:
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
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.4.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.simiam.Main</mainClass>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.handlers</resource>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.schemas</resource>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
maven-jar-plugin和maven-assembly-plugin插件打包之前有提过,用maven-jar-plugin与maven-dependences-plugin插件打包时,配置相关的资源文件也被打进jar包中,后续想修改配置信息将很头痛,同时这种打包方式无法解决部署文件目录结构复杂的场景。因此就有了使用maven-jar-plugin和maven-assembly-plugin插件打包的方式。
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
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.6</version>
<configuration>
<archive>
<manifest>
<mainClass>com.simiam.HttpIpv6StatisticTaskLauncher</mainClass>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
</manifest>
<manifestEntries>
<Class-Path>.</Class-Path>
</manifestEntries>
</archive>
<excludes>
<!-- **表示0到N级目录,即包括conf目录 -->
<exclude>conf/**/*.properties</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
<configuration>
<appendAssemblyId>false</appendAssemblyId>
<descriptors>
<descriptor>src/main/assembly/package.xml</descriptor>
</descriptors>
</configuration>
</plugin>
里面的关键就是package.xml这个文件了,它告诉插件具体应该如何组织打包等。
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
<?xml version="1.0" encoding="UTF-8"?>
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">
<id>package</id>
<formats>
<format>zip</format>
</formats>
<includeBaseDirectory>true</includeBaseDirectory>
<fileSets>
<fileSet>
<directory>src/main/resources</directory>
<outputDirectory>/</outputDirectory>
</fileSet>
<fileSet>
<directory>${project.build.directory}</directory>
<outputDirectory>/</outputDirectory>
<includes>
<include>*.jar</include>
</includes>
</fileSet>
</fileSets>
<dependencySets>
<dependencySet>
<outputDirectory>lib</outputDirectory>
<excludes>
<!-- <exclude>${project.name}-${project.version}</exclude> -->
<exclude>${groupId}:${artifactId}</exclude>
</excludes>
</dependencySet>
</dependencySets>
</assembly>
maven-assembly-plugin插件的详细介绍见官网。
maven-assembly-plugin插件和shell脚本打包当然借助maven-assembly-plugin插件与shell脚本配合也可以实现上面的功能,但相对复杂一些;能用上一种方式解决的就不要用shell在代替maven-jar-plugin插件的功能。这里简单提供示例的shell脚本与bat脚本。
脚本来源:
~/workspace/Java/news-recommend/recommend-sys/news-recommend/recommend-osf
shell脚本
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
#!/bin/bash
cd `dirname 0ドル`
BIN_DIR=`pwd`
cd ..
DEPLOY_DIR=`pwd`
LOGS_DIR=""
if [ -n "$LOGS_FILE" ]; then
LOGS_DIR=`dirname $LOGS_FILE`
else
LOGS_DIR=$DEPLOY_DIR/logs
fi
if [ ! -d $LOGS_DIR ]; then
mkdir $LOGS_DIR
fi
STDOUT_FILE=$LOGS_DIR/stdout.log
JVM_ERR_FILE=" -XX:ErrorFile=$DEPLOY_DIR/hs_err_pid%p.log "
CONF_DIR=$DEPLOY_DIR/conf
LIB_DIR=$DEPLOY_DIR/lib
LIB_JARS=`ls $LIB_DIR|grep .jar|awk '{print "'$LIB_DIR'/"0ドル}'|tr "\n" ":"`
JAVA_DEBUG_OPTS=""
if [ "1ドル" = "debug" ]; then
JAVA_DEBUG_OPTS=" -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=8787,server=y,suspend=n "
fi
JAVA_JMX_OPTS=""
if [ "1ドル" = "jmx" ]; then
JAVA_JMX_OPTS=" -Dcom.sun.management.jmxremote -Djava.rmi.server.hostname=localhost -Dcom.sun.management.jmxremote.port=1099 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false "
fi
JAVA_MEM_OPTS=""
BITS=`java -version 2>&1 | grep -i 64-bit`
if [ -n "$BITS" ]; then
JAVA_MEM_OPTS=" -server -Xmx4g -Xms4g -XX:NewRatio=1 -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m -Xss256k -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:LargePageSizeInBytes=128m -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/wwwroot/news-recommend-osf/ "
else
JAVA_MEM_OPTS=" -server -Xms1g -Xmx1g -XX:PermSize=128m -XX:SurvivorRatio=2 -XX:+UseParallelGC "
fi
cd $BIN_DIR
nohup java $JAVA_OPTS $JAVA_MEM_OPTS $JVM_ERR_FILE $JAVA_DEBUG_OPTS $JAVA_JMX_OPTS -classpath $BIN_DIR:$CONF_DIR:$LIB_JARS com.simiam.recommend.osf.RecommendOsfLauncher > $STDOUT_FILE 2>&1 &
echo "JAVA_JMX_OPTS: $JAVA_JMX_OPTS"
echo "OK!"
PIDS=`ps -f | grep java | grep "$DEPLOY_DIR" | awk '{print 2ドル}'`
echo "PID: $PIDS"
echo "STDOUT: $STDOUT_FILE"
bat脚本
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
@echo off & setlocal enabledelayedexpansion
set LIB_JARS=""
cd ..\lib
for %%i in (*) do set LIB_JARS=!LIB_JARS!;..\lib\%%i
cd ..\bin
if ""%1"" == ""debug"" goto debug
if ""%1"" == ""jmx"" goto jmx
java -Xms64m -Xmx1024m -XX:MaxPermSize=64M -cp ..\conf;%LIB_JARS% com.simiam.recommend.osf.RecommendOsfLauncher
goto end
:debug
java -Xms64m -Xmx1024m -XX:MaxPermSize=64M -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=8787,server=y,suspend=n -cp ..\conf;%LIB_JARS% com.simiam.recommend.osf.RecommendOsfLauncher
goto end
:jmx
java -Xms64m -Xmx1024m -XX:MaxPermSize=64M -Dcom.sun.management.jmxremote.port=1099 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -cp ..\conf;%LIB_JARS% com.simiam.recommend.osf.RecommendOsfLauncher
goto end
:end
pause
打war包的核心插件为maven-war-plugin,因为war包不涉及配置文件独立配置问题,同时其目录结构需要符合servlet规范,因此其打包方式相对简单,基本上只要参考官网即可。如果说非要注意的无非就以下一点:
servlet3.0及以上版本的来说,工程中可以不提供web.xml文件,如果用maven-war-plugin插件的旧版本(3.0.0版本以下)在打包时会报找不到web.xml文件而中止打包,在这种情况下需要在插件配置节点中设置<failOnMissingWebXml>false</failOnMissingWebXml>,举例如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.6</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
<!-- 使用web.xml文件路径 -->
<webXml>src/config/dev/web.xml</webXml>
<!-- 将某些需要的文件拷贝到WEB-INF下 -->
<webResources>
<resource>
<directory>src/config/dev</directory>
<targetPath>WEB-INF</targetPath>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</webResources>
</configuration>
</plugin>
我们会经常碰到这样的问题,在pom中引入了一个jar,里面默认依赖了其他的jar包。jar包一多的时候,我们很难确认哪些jar是我们需要的,哪些jar是冲突的。此时会出现很多莫名其妙的问题,什么类找不到啦,方法找不到啦,这种可能的原因就是jar的版本不是我们所设想的版本,但是我们也不知道低版本的jar是从哪里引入的。此时就出现maven-enforcer-plugin插件,其正是用于排查及解决此问题的有力工具。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>1.4.1</version>
<executions>
<execution>
<id>enforce</id>
<configuration>
<rules>
<dependencyConvergence/>
</rules>
</configuration>
<goals>
<goal>enforce</goal>
</goals>
</execution>
</executions>
</plugin>
在进行mvn clean package的时候,会在console中打印出来冲突的jar版本和其父pom,如下:
1
2
3
4
5
6
7
8
9
10
11
Dependency convergence error for org.slf4j:slf4j-api1.6.1 paths to dependency are:
[ERROR]
Dependency convergence error for org.slf4j:slf4j-api:1.6.1 paths to dependency are:
+-org.myorg:my-project:1.0.0-SNAPSHOT
+-org.slf4j:slf4j-jdk14:1.6.1
+-org.slf4j:slf4j-api:1.6.1
and
+-org.myorg:my-project:1.0.0-SNAPSHOT
+-org.slf4j:slf4j-nop:1.6.0
+-org.slf4j:slf4j-api:1.6.0
这个时候,我们看一眼就知道应该把哪个dependency中的哪个jar进行exclude。
插件详细配置说明见插件官网
因为spring-boot-maven-plugin插件已经为我们准备了一切,如果无特别需求,我们基本只需要简单引入即可:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.simiam.PerfHttpIpv6App</mainClass>
</configuration>
</plugin>
<!-- 因为spring-boot-maven-plugin未提供源码打包插件,所以我们需要自己添加 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.0.1</version>
<executions>
<execution>
<id>attach-sources</id>
<phase>verify</phase>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
还有非常多的插件,就不在这里一一介绍了,各位可以直接看官网
通过profile中定义的property,基于工程中的properties模板生成最终的properties文件。
利用profile实现多环境配置文件打包问题
profile是一组配置的集合,用来设置或者覆盖Maven构建的默认配置。通过profile配置,可以为不同的环境定制构建过程,例如Producation和Development环境。
Profile在pom.xml中使用activeProfiles或profiles元素指定,并且可以用很多方式触发。Profile 在构建时修改POM,并且为变量设置不同的目标环境(例如,在开发、测试和产品环境中的数据库服务器路径)。
| 类型 | 在哪里定义 |
|---|---|
| Per Project | 定义在工程pom.xml中 |
| Per User | 定义在Maven设置xml文件中(%USER_HOME%/.m2/settings.xml) |
| Global | 定义在Maven全局配置xml文件中(%M2_HOME%/conf/settings.xml) |
Maven的Profile能够通过几种不同的方式激活:
1
2
# dev是profile的ID
mvn install -Pdev
1
2
3
4
# 在setting.xml文件中添加如下信息,setting.xml位于上节介绍的三种profile类型的相应位置下。
<activeProfiles>
<activeProfile>dev</activeProfile>
</activeProfiles>
以下三种激活方式不常用,这里不作进一步介绍,有需要的请直接参考这里。
注意:profile节点下定义的元素都会覆盖其所在Pom文件中定义的同名的节点,如同名plugin,properties中的同名属性等。
比如对于springboot项目来说,可通过maven的profile中添加env属性:
1
2
3
4
5
6
<profile>
<id>dev</id>
<properties>
<env>dev</env>
</properties>
</profile>
当运行mvn install -Pdev时,会复制src/main/resources/application.yml模板文件复制至目标目录下,并将其中的spring.profiles.active: ${env} 配置项的${env}占位符替换成dev。
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
server:
port: 8080
servlet:
context-path: /perf
spring:
profiles:
active: ${env} # 将被替换成目标profile的env属性值
datasource:
name: perf-ds
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: org.postgresql.Driver
druid:
filters: stat
maxActive: 20
initialSize: 1
maxWait: 60000
minIdle: 1
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
maxOpenPreparedStatements: 20
mybatis:
mapper-locations: classpath:mapping/*.xml
type-aliases-package: com.simiam.http.entity
pagehelper:
helperDialect: postgresql
reasonable: true
supportMethodsArguments: true
params: count=countSql
---
spring:
profiles: dev
datasource:
url: jdbc:postgresql://localhost:5432/perf?currentSchema=dev
username: *****
password: *****
---
spring:
profiles: prod
datasource:
url: jdbc:postgresql://localhost:5432/perf?currentSchema=prod
username: *****
password: *****
最终生成的application.yml将被springboot应用程序使用,其会根据激活的profile选择对应的配置项,连接对应的数据库实例。
本节主要记录日常开发工作中使用maven所挖过、踩过、填过的坑。(不断更新)
import scope引入spring-boot-dependencies时无法引用POM中定义的property、pluginManagement等一系列问题 ]]>转载请注明出处:simiam.com
转载请注明出处:simiam.com
本地源码构建安装:
1
2
3
4
5
6
7
cd <hue_dir_path>
export LDFLAGS=-L/usr/local/opt/openssl/lib
export CPPFLAGS=-I/usr/local/opt/openssl/include
make clean
make apps
./build/env/bin/supervisor
登录默认用户名密码:admin/admin
]]>转载请注明出处:simiam.com
随着软件发布迭代的频率越来越高,传统的"瀑布型"(开发—测试—发布)模式已经不能满足快速交付的需求。2009年左右DevOps(Development和Operations的组合)应运而生,简单地来说,就是更好的优化开发(DEV)、测试(QA)、运维(OPS)的流程,开发运维一体化,通过高度自动化工具与流程来使得软件构建、测试、发布更加快捷、频繁和可靠。
DevOps的组成
DevOps出现以前,开发与运维对于生产环境上出现的问题是这样处理的:
运维人员:
It's not my machine
开发人员:
It's not my code
处理问题的模式:(未进行有效沟通,而是相互责备、报怨,在经过长时间的各种扯皮后把问题解决了)
Before apply DevOps
DevOps出现以后,开发与运维的关系转变为:
Think for peer
因此,他们对于生产环境上出现的问题是这样处理的:
Think for peer
处理问题的模式变为:(相互理解,充分沟通并最终快速解决问题)
After apply DevOps
DevOps集文化理念、实践和工具于一身,可以提高组织高速交付应用程序和服务的能力,与使用传统软件开发和基础设施管理流程相比,能够帮助组织更快地发展和改进产品。这种速度使组织能够更好地服务其客户,并在市场上更高效地参与竞争。
DevOps是一个完整的面向IT运维的工作流,以IT自动化以及持续集成(CI)、持续部署(CD)为基础,来优化程式开发、测试、系统运维等所有环节,其典型的运作流程如下:
DevOps-lifecycle
上图中的Build, Test, Release对应的CI/CD平台中的构建、测试、交付(部署)这几个概念,也就是说CI/CD是实施DevOps的基础,除此之外还会涉及源码质量管理平台(静态代码分析、潜在BUG分析、单元测试覆盖报表等)。
实施DevOps前,软件开发管理生命周期如下:
before apply devops
实施DevOps后,软件开发管理生命周期如下:
after apply devops
到底DevOps能给企业带来什么样的业务价值呢?DevOps平台的理念固然是将软件研发的全生命周期管理起来,但是并不意味着一定要做到全生命周期的管理,落实到企业内部,终究还是要结合企业的现状和实际的需求,有选择性有目标的去建设。对于企业而言,不管是提升IT的运营效率70%,还是做到开发测试环境的持续集成、自动化测试、自动化部署,亦或是一天部署10次这种DevOps最初的目标,最重要的还是要结合现状,先认清DevOps能给企业带来什么样的业务价值。
DevOps涉及到源码质量管理、持续集成、持续交付、持续部署、运维管理等多个系统平台,其天然适合那些主要提供线上服务的互联网团队(特别是基于微服务架构的团队)。而对于非互联网企业(主要是传统软件厂商)来说,其主要是为用户提交软件产品,基本不存在运维管理的需求。同时,传统软件厂商的软件开发管理生命周期中,其对源码质量管理、持续集成、持续交付的需求比较强烈,而持续部署主要是为了解决开发与测试环境自动化搭建问题。
企业引入DevOps也可能需要对企业组织架构进行调整,以解决部门墙问题。
很显然,我们暂时不需要引入DevOps这种软件开发模式。
持续交付依赖持续集成,而持续部署依赖持续交付,因此我们将这三者放在同一章节里介绍。
持续集成是一种软件开发实践,即团队开发成员经常集成他们的工作,通过每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽早地发现集成错误。
— 摘自
Martin Fowler的文章:Continuous Integration
传统软件开发模式下,通常情况下是在每个项目成员完成他们各自的任务后才开始进行系统集成。整个集成过程一般会长达数周甚至几个月的时间,在此期间经常出现让所有相关人员都十分痛苦的事情。而持续集成(CI)则将传统软件开发过程中的集成阶段大大提前了,只要有代码提交至源码库(如Git)就会触发构建(包括代码分析)、测试等任务,通过这种频繁的集成来防止传统模式下因集成问题堆积而导致"集成地狱(Integration Hell)"。
简单讲,持续集成就是不断对合并项目主干的源码进行相应的检查、构建、测试,通常每天至少进行一次;持续集成不是为了避免软件开发过程中的各种问题,而是为了尽量早的发现问题。
引入CI意味着:在家里远程办公的开发人员A与在公司办公的开发人员B可以很方便的在同一个项目中进行协同办公,而不需要担心源码合并问题,因为他们的每次源码提交操作都会触发CI进行构建、测试以验证他们的代码是否能正常工作。
开发人员通常会使用CI服务器来进行源码构建与集成,为了让持续集成流程有效的运行起来,项目组的所有开发人员都需要编写单元测试代码以保证其提交的代码能按预期的目标执行,同时不会影响其他人的工作成果。当所有的单元测试用例都执行通过后,这些集成后的代码还不能马上交付或部署至生产环境,因为在交付前还需要在集成测试环境(准生产环境)进行集成测试、验收测试(Acceptance Testing),这部分内容属于持续交付的范畴,下节将会详细介绍。
下图为持续集成的主要流程:
Continuous Integration
引入持续集成的好处:
持续交付(Continuous Delivery)是指在持续集成的基础上,将集成后的代码部署到更贴近真实运行环境的"准生产环境"(production-like)中。 比如,我们完成单元测试后,可以把代码部署到连接数据库的Staging环境中进行更多的测试。如果代码没有问题,可以继续手动部署到生产环境中。
Continuous Delivery
由上图可知,从开发一直到项目可交付整个过程中涉及的环境可能有:开发环境、测试环境(包括压测环境)、准生产环境,生产环境,这整个过程也常被称为部署管道(Deployment Pipeline),每个环境所执行的测试用例也将不一样,只有前一个环境的测试用例全部通过才能继续进入下一个环境(即自动部署新环境 + 执行适合该环境的测试用例);同时每个环境的执行结果都会及时反馈给相应的开发人员,如果环境部署异常或用例执行失败他们就能更容易更及时的解决问题。
整个过程基本按"部署环境 -> 执行测试用例 -> ... -> 部署环境 -> 执行测试用例"这一模式不断重复,因此为了实现自动化的持续交付,"自动化部署"、"可自动化执行的测试用例"是关键的前提条件。
有一点需要注意,对于提供线上服务的团队,当功能代码可交付时,还需要将相关代码部署至生产环境,但这一部署过程是纯手工的。这也是持续交付与持续部署的区别。
持续部署(Continuous Deploy)是在持续交付的基础上,把部署到生产环境的过程自动化。持续部署和持续交付的区别就是最终部署到生产环境是自动化的。
Continuous Deployment
这里大家也许有个疑问:在生产环境是否需要执行测试用例,如果需要执行,那会否影响系统性能或产生脏数据?
这个问题不好简单的回答是或否,从我自己的经验来讲:
一个项目要引入CI/CD(持续集成与持续交付),一般情况下需要具备以下几个条件:
通用指标分析结果:
CI/CD平台比对
上面的分析结果中,排除掉非开源与收费版的,就只剩下Jenkins,Gitlab CI,Go CD,Flow CI四个版本,而flow.ci为国内开源项目,根据国内开源项目的尿性(基本都是先开源后收费),个人认为也不适合使用Flow CI。那么就只剩下三个可选方案了。
Jenkins是一款用Java编写的开源的跨平台的CI工具,同时可以通过插件扩展功能。Jenkins插件非常好用,我们可以容易地添加自己的插件。除了它的扩展性之外,Jenkins还有另一个非常好的功能——它可以在多台机器上进行分布式地构建和负载测试。Jenkins是根据MIT许可协议发布的,因此可以自由地使用和分发。
总的来说:Jenkins是最好的持续集成工具之一,它既强大又灵活。但其UI比较古老,同时学习它可能要花费一些时间,如果你需要一个灵活的持续集成解决方案,那么学习如何使用它将是非常值得的。
GitLab CI是GitLab的一个组成部分,GitLab CI能与GitLab完全集成,可以通过使用GitLab API轻松地作为项目的钩子。GitLab的执行部分(流程构建)使用Go语言编写,可以运行在Windows,Linux,OSX,FreeBSD和Docker上。
官方的Go Runner可以同时运行多个作业,并具有内置的Docker支持。对基于Gitlab找寻私有源码仓库的公司来说,可以考虑使用Gitlab CI作为其CI/CD平台。
Go CD是ThoughtWorks公司Cruise Control的化身。让Go CD脱颖而出的是它的流水线概念,使复杂的构建流程变得简单。关于流水线概念是如何帮助持续交付,以及如何与Jenkins的流水线流程进行比较,可以参考这里。它最初在设计时就支持流水线概念,消除了构建过程的瓶颈,并能够并行地执行任务。
Jenkins也可以通过plugins机制引入流水线功能。
本文章只提供三个可选方案,每个方案各有其优势:
但具体选择哪一款,还是得根据项目团队的实际需要来定。对于我们组来讲,Jenkins基本能满足需求,个人认为可优先使用Jenkins并收集使用过程中碰到的问题与新需求;同时,在资源允许的情况下可事先安排人员深入分析这三个平台以后需。
随着IT行业中软件产品的推陈出新,客户对于软件产品的要求也越来越高,因此如何高质量的管理软件代码,及时地对代码质量进行分析并给出合理的解决方案就成为了当下必须要解决的一个问题。与当今众多的代码质量管理工具相比,SonarQube更具有特色和竞争力,其优势主要体现为:它是一个开源的代码质量管理系统,支持25+种语言,可以通过使用插件机制与eclipse、JIRA、Jenkins等其他外部工具集成,从而实现了对代码质量的全面自动化分析和管理。
SonarQube既可以本地安装也可以选择托管在云端,云端托管地址为:https://sonarcloud.io/。
可用的SonarQube管理平台:SonarQube Demo
SonarQube截图
Jenkins,CheckStyle,FindBug三者配合起来能够实现最基本代码分析需求,但其分析结果展现还需要再配合其他插件来实现,同时其展示界面比较古老,本质上讲该功能是依附于Jenkins的,并不是一个独立的代码质量管理平台。
自动化测试可以快速自动完成大量测试用例,节约巨大的人工测试成本;同时它需要拥有专业开发技能的人才能完成开发,且需要大量时间进行维护(在需求经常变化的情况下),所以大部分具有很好开发技能的人员不是很愿意编写自动化用例。但由于软件规模的高速增长,人力资源的逐步稀缺,自动化测试已是势在必行。
对于自动化测试首先需要保证其功能是对客户有价值的和正确可用的。而这一切的基础就是用例要能测试客户的需求,期望,最好能让客户参与到测试用例的开发过程中来或让客户评审测试用例,因此出现了ATDD、BDD等各种理论方法来支撑这一行为。现有很多自动化测试工具可支持ATDD、BDD等,比如Cucumber、RobotFramework、SpecFlow、JBehave、Fitness、Concordion、Guage等,其中Cucumber和RobotFramework是最流行的两个框架,本文将简单分析这两个框架(想要完全搞懂这两个框架还是需要安排相关资源投入分析):
Cucumber VS RobotFramework
单元测试的思路是在不涉及依赖关系的情况下测试代码(隔离性),所以测试代码与其他类或者系统的关系应该尽量被消除。一个可行的消除方法是替换掉依赖类(测试替换),也就是说我们可以使用替身来替换掉真正的依赖对象。
常见的测试模拟类的分类如下:
dummy object:其作为参数传递给方法但是绝对不会被使用。比如说,这种测试类内部的方法不会被调用,或者是用来填充某个方法的参数。Fake:它是真正接口或抽象类的实现类,但其对象内部实现很简单。比如说,它存在内存中而不是数据库中。(即:Fake实现了真正的逻辑,但它的存在只是为了测试,而不适合于用在产品中。)stub:Stub类是依赖类的部分方法实现,而这些方法在进行类和接口的测试时会被用到,也就是说stub类在测试中会被实例化。stub类会回应任何外部调用。stub类有时候还会记录调用的一些信息。mock object:其是指类或者接口的模拟实现,我们可以自定义这个对象中某个方法的输出结果;Stub和Mock虽然都是模拟外部依赖,但Stub是完全模拟一个外部依赖, 而Mock还可以用来判断测试通过还是失败。我们可以手动创建一个Mock对象或者使用Mock框架来模拟这些类,Mock框架允许你在运行时创建Mock对象并且定义它的行为。
一个典型的例子是把Mock对象模拟成数据的提供者。在正式的生产环境中它会被实现用来连接数据源。但是我们在测试的时候Mock对象将会模拟成数据提供者来确保我们的测试环境始终是相同的。
通过Mock对象或者Mock框架,我们可以测试代码中期望的行为。
静态方法、构造方法、私有方法以及Final方法来说,EasyMock与Mockito都无能为力,需要结合PowerMock来搞定)。Mockito与EasyMock的比较可参考:https://github.com/mockito/mockito/wiki/Mockito-vs-EasyMock
在介绍Foreman之前,我们先介绍下[Bare Metal Provisioning]这个术语(英语不好,暂且翻译为"裸机供应")。所谓Bare Metal Provisioning是指为一台服务器(物理机或虚拟机)安装指定的操作系统的过程。通常情况下,有两种办法为服务器安装操作系统:
Foreman就是这样的软件工具,类似的还有Razor, Cobbler, RackHD。当然Foreman除了"裸机供应"能力外,它还有提供了其它服务,如与Puppet, Chef配合实现自动化配置与管理,它还提供了一个可视化的管理平台。更详细的信息还请直接访问官网。
Foreman是用Ruby语言开发的。
Ansilbe是一个部署一群远程主机的工具。远程的主机可以是远程虚拟机或物理机, 也可以是本地主机。
Ansible与Open Source Puppet一样,不具备"裸机供应"能力,其也需要与Foreman或ansible-os-autoinstall等工具配合使用。
Ansible最初是作为自动化工具(如PupPET和Chef)的轻量级替代而开发的;但是它是用Python编写的(而不是Ruby),并且采用了一个基于SSH的无代理的架构(即不是采用Puppet的C/S架构,在被管理节点上不需要安装任何特殊软件)。
Ansilbe通过SSH协议实现远程节点和管理节点之间的通信,但是SSH必须配置为公钥认证登录方式,而非密码认证。理论上说,只要管理员通过ssh登录到一台远程主机上能做的操作,Ansible都可以做到。包括:
总得来说,Ansible有如下特点:
1
2
3
4
5
6
7
8
1、部署简单,只需在主控端部署Ansible环境,被控端无需做任何操作;
2、默认使用SSH协议对设备进行管理;
3、有大量常规运维操作模块,可实现日常绝大部分操作。
4、配置简单、功能强大、扩展性强;
5、支持API及自定义模块,可通过Python轻松扩展;
6、通过Playbooks来定制强大的配置、状态管理;
7、轻量级,无需在客户端安装agent,更新时,只需在操作机上进行一次更新即可;
8、提供一个功能强大、操作性强的Web管理界面和REST API接口——AWX平台。
Ansible是用Python开发的,它目前已被Redhat收购,是自动化运维工具中认可度最高的。oVirt的很多自动化部署子项目都是用Ansible开发的。
Puppet是开源的系统配置管理工具,基于C/S的部署架构。是一个为实现数据中心自动化管理而设计的配置管理软件,它使用跨平台语言规范,管理配置文件、用户、软件包、系统服务等。Puppet开源项目创建之初主要是为了解决配置管理自动化的相关问题,该项目目前存在两种形态:
Puppet也是用Ruby语言开发的。
由于Ansible与Puppet属于同一类型的工具,使用哪一种则由团队成员的技能储备决定,就我们组来说,个人觉得Ansible更合适:a.团队有python储备;b.被管节点不需要安装代理。
本文主要介绍了CI/CD方案的常见流程及相关的工具集,但对于大部分工具并未进行深入的学习与分析,因此接下来需要安排专门的资源深入学习,特别是自动化测试相关框架的学习。
ovirt-engine为虚拟化环境提供了一个集中管理的平台,用户可以根据具体的需求选择使用不同的方式来访问ovirt-engine。
ovirt-engine架构图如下:
ovirt-engine架构图
Wildfly是一个JavaEE应用服务器,但是需要注意的是部署ovirt-engine所需要的wildfly是特殊定制过的,因此您不能将其它应用部署在这个为ovirt-engine专门定制的wildfly上,为了便于与通用版本区分开来,本文档后续将定制版本统称为ovirt-wildfly。
桌面系统虚拟化为用户提供了一个近似于物理PC的桌面系统。用户可以使用网络浏览器来访问用户门户,并通过用户门户来访问分配给他们的虚拟桌面资源(SPICE/VNC)。系统管理员需要为每个用户设定他们可能访问的资源及相关权限。User Portal的用户有两种类型:标准用户、高级用户。标准用户可以启动、停止和使用分配给他们的虚拟桌面(无法编辑虚拟机配置),而高级用户则还可以执行一些额外的管理任务(如创建、删除、编辑虚拟机,管理虚拟磁盘和网络接口等)。
管理门户是ovirt-engine的一个图形管理界面。管理员可以通过该管理界面来监控、创建并维护整个ovirt虚拟环境中的所有资源(计算、网络、存储)。通过管理门户可执行如下操作:
- 创建和管理虚拟基础架构(网络、存储域)
- 创建和管理虚拟机
- 创建和管理逻辑项(数据中心、集群)
- 安装和管理计算节点(虚拟主机)
- 用户权限管理
详细的使用文档请查阅官方文档。
目录服务提供了一个基于网络的、集中存储的用户与机构信息。这些信息包括应用程序的设置、用户档案资料、用户组数据、策略信息和访问控制信息。ovirt-engine支持Active Directory,Identity Management,OpenLDAP和Red Hat Directory Server 9所提供的目录服务。另外它还包括了一个本地内部域(internal),这个域只被用来进行系统管理,并只包含一个用户:admin。
ovirt-engine在安装时会同时安装一个数据仓库,它被用来收集监控主机、虚拟机和存储所产生的数据。ovirt-engine预先提供了一组报表,用户也可以使用任何支持SQL查询的工具来创建新的报表。
ovirt-engine在安装过程中会在Postgres数据库服务器上创建两个数据库:engine与ovirt_engine_history。
engine:它是ovirt-engine用来保存数据的主数据库,它保存了与虚拟环境相关的数据(如虚拟环境的状态、配置等数据)。
ovirt_engine_history:该数据库保存了配置信息和各种性能统计数据,这些数据是根据不同时从engine数据库中收集来的。系统在每分钟都会检查engine数据库中的配置,任何改变都会被记录在ovirt_engine_history数据库中。这些信息可以用来帮助管理员分析和提高ovirt虚拟化环境的性能及协助管理员解决存在的问题。
ovirt-engine的报表功能是通过ovirt-engine-dwhd服务实现的。
管理员除了通过管理门户来管理维护虚拟化环境外,还可以通过CLI-API或REST-API来与外部管理系统做进一步的整合:
- 把虚拟环境集成到IT环境中
- 与第三方虚拟化软件集成
- 自动化维护和错误检查任务
- 使用脚本执行重复性操作
一个ovirt虚拟化环境包括一个或多个用于运行虚拟机的计算节点(我们通常称为虚拟主机、物理主机、主机或计算节点)。一个普通的物理主机要成为ovirt虚拟化环境的计算节点有如下两种办法:
具体安装配置文件请参考官方文档
ovirt-node的主机核心架构图如下:
ovirt-node架构图
KVM是一个可加载的内核模块,它通过使用Inter-VT或AMD-V等硬件虚拟化扩展技术来提供虚拟化功能。KVM本身运行于内核空间,而在它上面运行的虚拟机服务会作为独立的QEMU进程在用户空间中运行。KVM允许主机把它的物理硬件资源分配给虚拟机。
QEMU是一个多平台的,提供全仿真功能的仿真器(emulator),它会仿真包括一个或多个处理器以及外设在内的整个系统。QEMU、KVM及带有虚拟化功能扩展的处理器组合在一起就可以提供虚拟化功能。
VDSM相当于ovirt-engine在ovirt-node上的一个代理服务进程。ovirt-engine通过VDSM暴露的API来管理ovirt虚拟化环境下的所有资源(计算、网络、存储),执行相关的监控配置任务、数据统计、日志收集。每个计算节点上都会运行一个VDSM服务(监听端口号默认为54321)来接收ovirt-engine发出的管理命令。
libvirt是一个底层工具库,它被用来协调虚拟机及其相关虚拟设备的管理。即当ovirt-engine发出一个操作虚拟机相关的命令时(如启动虚拟机、停止虚拟机),VDSM会调用目标计算节点上的libvirt的相应指令来执行这个操作。
SPM即存储池管理器,它是ovirt虚拟化环境中与存储管理相关的重要概念。SPM是分配给一个数据中心中某个主机(计算节点)角色,即一个数据中心中的所有主机中,具有SPM角色的主机只能有一个,我们称其为SPM主机。SPM主机全权负责数据中心中所有与存储、存储域管理相关的业务。SPM角色可以在同一个数据中心中的不同主机间进行迁移,同一个数据中心中的所有主机都必须能访问这个数据中心中定义的所有存储域。ovirt-engine会确保SPM一直处于有效的状态,如果SPM所在的主机出现问题,ovirt-engine会重新发起SPM选举确定新的SPM主机。
"数据中心"是ovirt虚拟化环境的重要概念,一个ovirt虚拟化环境中可以有多个数据中心。"存储域"则是ovirt虚拟化环境中存储资源管理的核心概念,下文会有专门的章节介绍。
数据中心是ovirt虚拟化环境中的根容器(最高一级的逻辑项),它包括以下三个子容器(子项):
集群容器用来保存与集群相关的信息。集群由一个至多个计算节点(主机)组成,这些主机具有相互兼容处理器内核。一个集群组成了一个虚拟机的迁移域,虚拟机可以被实时迁移到同一集群中的其它主机上。一个数据中心可以包括多个集群,一个集群可以包括多个主机。
存储容器用来保存存储类型、存储域的信息,以及存储域间的连接信息。存储域在数据中心一级上定义,并可以被数据中心的所有集群使用(即可以被集群中的所有主机挂载)。
网络容器用来保存与数据中心中的逻辑网络相关的信息,如网络地址、VLAN标签等信息。逻辑网络在数据中心一级上定义,并可以在集群一级上使用。
ovirt虚拟化平台使用一个集中的存储系统来保存虚拟磁盘镜像、模板、快照和ISO文件。存储被分为由各个存储域组成的存储池,一个数据中心只有一个存储池。存储域由一组存储空间和代表这些存储空间内部结构的元数据所组成。目前有三种存储域类型:数据存储域,导出存储域,ISO存储域,三种存储域的介绍见后续章节。
每个数据中心都必须包括一个数据存储域,而每个数据存储域也只能关联一个数据中心。存储域是用于提供资源共享的,因此数据中心的所有主机都必须能访问所有存储域。
ovirt虚拟化环境的网络架构提供该环境中不同对象间的网络连接,同时它还可以用来实现网络隔离。其网络架构图如下:
ovirt网络架构图
用来处理虚拟机间网络连接的逻辑网络是通过计算节点上的基于软件的网桥实现的。在默认情况 下,ovirt-engine在安装过程中会创建一个名为"ovirtmgmt管理网络"的逻辑网络。此外系统管理员还可以添加专用的存储逻辑网络和专用的显示逻辑网络。
在介绍ovirt虚拟化环境的存储相关内容前,我们需要简单了解下几种常见的存储服务类型:块存储,文件存储,对象存储。
下图是常见存储服务类型的系统层级分布图:
存储类型系统层级分布图
我们从底层往上看,最底层就是硬盘,多个硬盘可以做成RAID组,无论是单个硬盘还是RAID组,都可以做成PV,多个PV物理卷捏在一起构成卷组(VG)。接下来可以从卷组上切出很多个逻辑卷(LV)。到LV这一层为止,数据一直都是以块(Block)的形式存在的,这时候提供出来的服务就是块存储服务。你可以通过FC协议或者iSCSI协议访问LV,映射到主机端本地,成为一个裸设备。在主机端可以直接在上面安装数据库,也可以格式化成文件系统后交给应用程序使用,这时候就是一个标准的SAN存储设备的访问模式,网络间传送的是块。
如果不急着访问,也可以在本地做文件系统,之后以NFS/CIFS协议挂载,映射到本地目录,直接以文件形式访问,这就成了NAS访问的模式,在网络间传送的是文件。
如果不走NAS,在本地文件系统上面部署OSD服务端,把整个设备做成一个OSD,这样的节点多来几个,再加上必要的MDS节点,互联网另一端的应用程序再通过HTTP协议直接进行访问,这就变成了对象存储的访问模式。当然对象存储通常不需要专业的存储设备,前面那些LV/VG/PV层也可以统统不要,直接在硬盘上做本地文件系统,之后再做成OSD,这种才是对象存储的标准模式,对象存储的硬件设备通常就用大盘位的服务器。
物理卷(PV)、卷组(VG)、逻辑卷(LV)及LVM相关的介绍请查阅LVM文档。
存储域就是一系列具有公共存储接口的镜像的集合,存储域中包括了虚拟机模板、快照、数据镜像、ISO文件以及存储域本身的元数据。一个存储域可以由块设备(块存储)组成,也可以由文件系统(文件存储)组成。
一般情况下,ovirt虚拟化环境并不直接管理基于文件的存储(它们都由外部系统负责管理),如NFS存储由NFS服务器或其它第三方网络存储服务器来管理。主机则可以管理它自身的本地文件存储系统。
块存储使用未格式化的块设备,这些块设备(相当于PV)被LVM合并为卷组,VDSM会通过扫描卷组来在LVM之上添加集群逻辑。当VDSM发现卷组变化时,它会通知相应的主机来更新它们的卷组信息。主机会把卷组分为不同的逻辑卷(LV),并在磁盘上保存逻辑卷的元数据。当在已存在的存储域中添加新的存储空间时,ovirt-engine会通知每台主机上的VDSM来更新卷组信息。
SPM主机上的LVM会处理块存储环境中发生的变化(如创建逻辑卷、扩展或删除逻辑卷、添加新的 LUN),然后SPM主机上的VDSM会将这些变化信息(元数据)同步至集群中的所有主机。
ovirt虚拟化平台支持的存储域可以被分为以下几类:
数据(Data)存储域:保存ovirt虚拟化环境中的所有虚拟机的磁盘镜像。这些磁盘镜像包括安装的操作系统,或由虚拟机产生或保存的数据。数据存储域支持NFS、iSCSI、FCP、GlusterFS或POSIX兼容的存储系统。
导出(Export)存储域:它可以在不同数据中心间转移磁盘镜像和虚拟机模板提供一个中间存储,并可以用来保存虚拟机的备份。导出存储域支持NFS存储。一个导出域可以被多个不同的数据中心访问,但它同时只能被一个数据中心使用。
ISO存储域:用来存储ISO文件(也称为镜像,它是物理的CD或DVD的代表)。在ovirt虚拟化环境中,ISO文件通常代表了操作系统的安装介质、应用程序安装介质和guest代理安装介质。这些镜像会被附加到虚拟机上,并象使用物理安装介质一样启动系统。ISO存储域为数据中心中的所 有主机提供了一组共享的ISO文件,这样所有的主机就不再需要物理的安装介质了。
"导出存储域"在以后的版本中将可能被弃用,因为最新版本的ovirt虚拟化平台的"数据存储域"已经具备导出存储域的功能了(从一个数据中心移除并挂载到另一个数据中心)。
QCOW2是虚拟磁盘镜像的一种存储格式,使用QCOW2格式可以把物理存储层和逻辑存储层分隔开。QCOW2为逻辑块和物理块之间创建了一个映射,每个逻辑块都会被映射到相应的物理块上;另外,QCOW2可以只保存物理存储上变化的数据。因为这个特性是存储空间"over-commitment"功能和虚拟机快照功能得以实现的基础。
初始的映射信息会把所有的逻辑块与物理文件系统或卷中对应的块相关联。在创建虚拟机快照后, 如果这个虚拟机需要向QCOW2卷写数据,系统会根据映射信息在物理存储中找到相应的块,并把新数据写到块中,然后只在新的快照QCOW2卷中记录数据的变化,并更新相应的映射信息。
当虚拟磁盘的镜像是RAW格式时,它上面的数据将没有特定的格式,对虚拟磁盘的操作也不需要主机进行特殊处理,因此使用RAW格式的虚拟机磁盘会比使用QCOW2格式的虚拟磁盘有更好的性能。当虚拟机向虚拟磁盘写数据时,I/O系统会在物理存储和逻辑卷中写相同的数据。RAW格式的虚拟磁盘在创建时就会被分配和所定义的镜像大小相同的存储空间(预先分配)。RAW镜像文件有很多优点,但它的一个非常大的缺陷就是不支持快照。
虚拟磁盘镜像所需要的所有存储空间在虚拟机创建前就需要被完全分配。如果虚拟机需要一个20GB的磁盘镜像,存储域中的20GB的存储空间就会被占用。因为在进行写操作时不需要进行磁盘空间分配的动作,所以预分配存储策略有更好的写性能。但是,预分配存储的大小不能被扩展,这就失去了一些灵活性。另外,它也会降低ovirt-engine进行存储"over-commitment"的能力。预分配存储策略适用于需要大量I/O操作(特别是写操作比较频繁时),并对存储速率有较高要求的虚拟机,一般情况下,作为应用服务器的虚拟机推荐使用预分配存储策略。
这种存储分配策略也叫存储精简配置策略。在创建虚拟机的时候,为虚拟磁盘镜像设定一个存储空间上限,而磁盘镜像在开始时并不使用任何 存储域中的存储空间。当虚拟机需要向磁盘中写数据时,磁盘会从存储域中获得一定的存储空间(默认为1G),当磁盘数据量达到所设置的磁盘空间上限时将不会再为虚拟磁盘增加容量。
如果ovirt环境底层的存储设备提供了精简分配(thin provisioning)功能,则我们创建虚拟磁盘时应该首选使用底层存储设备所提供的能力来实现精简配置。我们通过图形用户界面为虚拟机配置存储时,应该选择"预分配存储策略",底层存储自己会实现精简配置的功能。
存储元数据包括:存储域元数据,存储池信息,模板与虚拟机镜像元数据。
ovirt虚拟化平台存储域元数据模型目录有三个版本(V1, V2, V3),当前使用的是V3版本。
元数据不同版本间的区别主要是:元数据格式,存储方式的变更。
每个存储域的元数据包括:存储域本身的结构、所有被虚拟磁盘镜像使用的物理卷的名字。
主存储域(Master Storage Domain)的元数据还额外包括了存储池中的所有域和物理卷的名字。因为这个元数据的大小不能超过2KB,所以它限制了一个池中所能包括的存储域的数量。
模板和虚拟机的基本数据镜像是只读的。
V1元数据适用于NFS、iSCSI和FC存储域。
所有存储域和池的元数据以逻辑卷标签的形式保存(不再被写到一个逻辑卷上)。而虚拟磁盘卷的元数据仍然以一个逻辑卷的形式保存在存储域中。元数据将不再包括物理卷名。
模板和虚拟机的基本数据镜像是只读的。
V2元数据适用于iSCSI和FC存储域。
所有存储域和池的元数据以逻辑卷标签的形式保存(不再被写到一个逻辑卷上)。而虚拟磁盘卷的元数据仍然以一个逻辑卷的形式保存在存储域中。
虚拟机和模板的基本镜像数据不再是只读的了。这使实时快照、实时存储迁移和快照克隆成为可能,支持unicode元数据,这样它就可支持非英文的卷名。
V3元数据适用于NFS、GlusterFS、POSIX、iSCSI和FC存储域。
ovirt-engine在启动时会将每个数据中心中的存储域配置信息下发给各个主机上的VDSM实例,VDSM分根据接收到的存储域配置信息将相关存储域挂载至主机。挂载成功后,你将可以在每个主机的如下目录查看到存储域的相关信息(包括元数据):
1
2
# 元数据存储目录
/rhev/data-center/<storage_pool_uuid>
下图是V3版元数据存储目录结构示意图:
V3版元数据存储目录结构示意图
注意: 文件存储类型的存储域与块设备类型的存储域的元数据存储位置是不同的,块设备类型存储域的元数据是存放在主存储域(马上到!)对应的卷组(VG)的标签(Tag)中,而不是dom_md目录下的metadata文件中。dom_md/metadata存放的是该存储域下虚拟机磁盘镜像(LV)的元数据集,每个虚拟机磁盘镜像(LV)的元数据存放在metadata块设备存储空间的某一个索引位置上,该索引位置信息则保存在虚拟机磁盘镜像所在的逻辑卷(LV)的标签(Tag)上。
相关查询命令:
vgs, lvs等,块设备存储域元数据的具体细节由VDSM负责维护,本文档不深入介绍。
主机根据其所在数据中心中的存储域元数据信息来监测存储域,当该数据中心中的所有主机都报告某个存储域无法访问时,这个存储域会被认定为"不活跃"。
但ovirt-engine监测到某个存储域不活跃时并不会断开与它的连接,而是会认为这可能是一个临时的网络故障导致的,engine会每隔5分钟尝试重新激活任何不活跃的存储域。
虽然系统管理员可能需要人为排除存储域连接问题的故障,但ovirt-engine会在连接问题解决后自动重新激活存储域。
ovirt虚拟化平台使用元数据来描述存储域的内部结构。结构元数据会被写到每个存储域的一个数据段中,它被用来记录镜像和快照的创建删除,以及卷和域的扩展操作。所有主机会使用"一人写,多人读"的机制来处理存储域元数据。这个能修改存储域元数据的主机就是SPM主机(后面简称SPM)。
下图是存储域元数据读写数据流示意图:
存储域元数据读写数据流示意图
SPM负责协调数据中心内所有存储域元数据的变更操作,比如创建删除磁盘镜像、创建合并快照、在存储域之间复制镜像、创建虚拟机模板、块设备存储分配等。一个数据中心只能有一个SPM主机,数据中心中的其它主机对存储域元数据信息只有读权限。
一个主机(计算节点)有两种方式升级为SPM主机:
当ovirt-engine通过上述任意一种方式设置某一主机为SPM时,ovirt-engine将向该主机上的VDSM发送一个spmStart的命令,其将尝试获取一个叫storage-centric的租约。如果成功获取到租约,则该主机就升级为SPM主机并一直持有该storage-centric租约,直到失去SPM角色或该SPM主机因为异常被ovirt-engine移出集群或数据中心。
拥有storage-centric租约的主机(肯定是SPM主机)对存储域元数据具有"写"权限。
该租约之所以被命名为storage-centric是因为该租约是直接被写入到主存储域(Master Storage Domain)上的一个特殊的名字叫leases的逻辑卷上(假设底层存储为块存储,文件存储则是写到一个名为leases的文件)。
主存储域(Master Storage Domain)简称马上到!,VDSM会把数据中心中第一个被创建的存储域(SD)作为马上到!。对于块存储来讲,磁盘镜像等虚拟机相关的元数据都存放在马上到!上。
当出现以下几种情况时,ovirt-engine将会发起新一轮的SPM主机选举:
如果没有主机被手工指定为SPM且出现上述任意一种情况时,ovirt-engine将会启动SPM选举过程:
Step 1: ovirt-engine会要求VDSM确认哪个主机已经拥有"storage-centric租約"。
Step 2: ovirt-engine会跟踪从存储域创建以来的SPM分配记录,并根据如下三个方面来判断主机是否可以作为SPM:
Step 3: 如果当前的SPM主机可以正常工作,这个主机就会继续持有"storage-centric租約",同时ovirt-engine把这个主机标识为SPM,并退出选举过程。
Step 4: 如果当前的SPM主机没有响应,则它会被认为处于"无响应"状态。如果那个主机已经配置了电源管理功能,它会被自动隔离(fence)。如果自动隔离失败,就需要手工隔离;在当前的SPM被隔离前,SPM的角色不能分配给其它主机;如果隔离成功则进行下一步。
Step 5: 当SPM角色和storage-centric租約都空闲时,ovirt-engine会把它们分配给数据中心中的一个SPM权重高的主机,如果所有候选主机的SPM权重都一样,则会随机选取一个主机。分配成功后ovirt-engine会将该该主机标记为SPM并退出选举过程;否则进行下一步。
Step 6: 如果为一个新主机分配SPM角色的操作失败,ovirt-engine会把这个主机加入到一个列表(该列表包括了所有分配SPM角色失败的主机,这个列表中的主机被标记为本轮选举无法成为SPM。这个列表中的内容会在开始下一次进行SPM选举的过程前被清除,从而使这个列表中的主机又有机会成为SPM)。ovirt-engine会一直尝试把SPM角色和storage-centric租約分配给一个主机,直到有一个主机被成功选举为SPM。
ovirt虚拟化环境中的一些资源具有排它性,每个排它性资源同时只能被一个线程访问。比如SPM就是一个具有排它性的资源。在一个数据中心中,同时只能有一个主机具有SPM角色,如果数据中心中存在多个具有SPM角色的主机,就会出现同一份数据同时被不同的主机修改的情况,从而造成数据被破坏(脏数据)。
在ovirt-v3.1之前,SPM的排它性是通过VDSM中的一个名为safelease的功能来实现的。这个租約被写到数据中心中的所有存储域中的一个特殊区域中,而数据中心中的所有主机都可以通过它来检查SPM的状态。VDSM的"safelease"的唯一功能就是来维持SPM的排它性。在3.1版本之后ovirt引入了Sanlock来实现资源互斥访问功能,它除了可以锁定SPM外,还可以"锁定"其它资源。因此,Sanlock具有更高的灵活性。
需要进行资源锁定的应用程序可以在Sanlock中进行注册,已经注册的应用程序可以请求Sanlock锁定某个资源,从而使其它程序无法访问被锁定的资源。例如,VDSM可以请求Sanlock锁定SPM资源,而不需要自己锁定它。
每个存储域都有一个lockspace区,锁定状态被记录在lockspace的磁盘中。因为SPM资源只能分配给一个"活跃的"主机,因此Sanlock就需要检查SPM主机是否是"活跃的"。当SPM主机连接到存储域时,它会更新从ovirt-engine获得的hostid,并且会定期在lockspace 中写一个时间戳(timestamp)。ids逻辑卷会记录每个hostid,并在每个主机更新它的hostid时进行相应的更新。Sanlock会根据主机的hostid和时间戳来决定它是否处于"活跃"状态。
资源的使用情况被记录在leases逻辑卷的磁盘中。当磁盘中代表某个资源的数据被更新为带有某个进程的id时,系统就认为该资源被这个进程所占用。具体到SPM角色资源,当它被占用时,它的数据会被更新为SPM主机的hostid。
每个主机上的Sanlock只需要检查资源一次来决定它们是否被占用。在初始的检查后,Sanlock只需要监测lockspaces中的相应主机的时间戳的状态。Sanlock需要监测使用资源的应用程序。对于VDSM,它会监测SPM的状态和hostid。如果主机无法从ovirt-engine重复获得它的 hostid,这个主机就会失去它所占有的、在lockspace中记录的所有资源的排它性。 Sanlock会更新相应的资源记录来标识这些资源不再被占用。
当SPM主机在一定的时间内无法更新存储域的lockspace中的时间戳时,这个主机上的Sanlock会要求VDSM进程释放它所占用的资源。
如果VDSM进程接受了这个请求,它将释放它所占用的资源(即lockspace中的SPM资源就可以被其它主机使用)。
如果SPM主机上的VDSM无法接受释放资源的请求,主机上的Sanlock就会使用kill命令来终止VDSM进程。如果kill命令运行失败,Sanlock会使用sigkill命令来终止VDSM进程。如果sigkill命令仍然无法终止进程,Sanlock将会通过watchdog守护进程来重启Sanlock所在的主机。
每次当主机的VDSM更新它的hostid并更新lockspace中的时间戳时,watchdog守护进程都会收到一个pet。 当VDSM不能进行这些操作时,watchdog守护进程将无法收到pet。如果watchdog守护进程在一定时间内仍然没有收到pet,它将会重启主机。这将保证SPM资源可以被释放,从而可以被其它主机使用。
有关Sanlock的详细原理请参考这里。
ovirt虚拟化平台的网络可以从3个方面介绍:基本网络、集群网络和主机网络配置。
一个有良好设计的网络将会为用户提供一个高性能的网络,并可以顺利实现虚拟机的迁移。而一个没有经过良好设计的网络可能为系统的使用和维护带来许多问题(如网络响应速度太慢、虚拟机迁移和克隆失败等)。
ovirt虚拟化平台通过使用以下网络元素为虚拟机、主机、虚拟(逻辑)网络提供网络服务:
网卡,网桥,虚拟网卡使得主机,虚拟机,本地网络,外部网络之间可以相互通信。而Bond与VLAN虽然是可选的网络元素,但它们可以增强网络的安全、性能及容灾能力。
网络接口控制器(Network Interface Controller),简写为NIC,通常被称为网卡,它是一个用于将计算机连接至网络的网络适配器。NIC工作在网络层和数据链路层来为所在的机器提供网络连接功能。在ovirt虚拟化环境中的主机都最少需要有一个NIC,但通常情况下主机都会有多个网卡。
一个物理网卡可以有多个虚拟网卡(vNIC)和它相连,而一个虚拟网卡可以看做是一个虚拟机的物理网卡。为了区分虚拟网卡和物理网卡,ovirt-engine会为每个虚拟网卡分配一个独立的MAC地址(在ovirt-engine可以配置MAC地址池)。
网桥与NAT的关系?
网桥(bridge)是数据包交换网络中进行数据包转发的软件设备。通过使用网桥,多个网络接口设备可以共享同一个物理网卡的连接,而每个网络接口设备都会以独立的物理设备的形式出现在网络中。网桥会检查一个数据包的源地址来决定相关的目标地址,一旦获得了目标地址的信息,它会在一个表中添加这个地址以供以后使用。通过使用网桥,主机可以把网络数据重新定向到使用虚拟网卡的、相应的虚拟机上。
在ovirt虚拟化环境中,逻辑网络是通过网桥来实现的。一个IP地址会分配给网桥而不是主机本身的物理网络接口,这个IP地址不需要和连接到这个网桥上的虚拟机的网络处于同一个子网中。如果分配给网桥的IP地址和虚拟机处于同一个子网中,网桥所在的主机将可以被虚拟机直接访问,我们通常不推荐在主机上运行可以被虚拟机访问的网络服务。虚拟机通过它们的虚拟网卡连接到逻辑网络中,虚拟机上的每个虚拟网卡都可以通过DHCP或静态分配的方式来获得IP地址。如果有需要,网桥也可以连接到主机以外的系统。
网桥和以太网连接都可以设置自定义属性。VDSM会把网络定义和自定义属性传给设置网络的hook脚本。
绑定(bond)是由多个网卡组合成的一个单一的、由软件定义的网络设备。因为一个绑定(bond)是由多个网卡组成的,因此它可以提供比单一网卡更高的网络传输速度,并提供了更好的网络容错功能(绑定只有在所有的网卡 都出现问题时才会停止工作)。绑定设备有一个限制:绑定必须由相同型号的网卡组成,同时有些绑定。
绑定设备的数据包传输算法是由绑定的模式所决定的,绑定模式共有7种(mode-0 ~ mode-6),其中mode-1 ~ mode-4支持虚拟机网络(使用网桥)和非虚拟机网络(无网桥);mode-0、mode-5、mode-6只支持非虚拟机网络(无网桥)。
ovirt虚拟化平台默认使用的是mode-4。绑定模式详细介绍请看这里。
虚拟网卡是基于主机的物理网卡的虚拟网络接口。每一个主机可以有多个物理网卡,而每个物理网卡可以有多个虚拟网卡。
当我们为虚拟机添加一个虚拟网卡时,ovirt-engine会在虚拟机、虚拟网卡本身和虚拟网卡所基于的主机的物理网卡间创建一定的关联。当虚拟机第一次启动时,libvirt会为虚拟网卡分配一个PCI地址。这样,虚拟机就可以使用MAC地址和PCI地址(如eth0)指定虚拟网卡。
如果虚拟机是通过模板或快照创建的,分配MAC地址以及把这些MAC地址和PCI地址相关联的过程会有所不同。
创建后,虚拟网卡会被添加到网桥设备中。网桥将用来处理虚拟机和虚拟机网络的连接。
在主机上执行
ip addr show命令可以显示基于该主机物理网卡所创建网桥等信息,我们还可以用brctl show命令来显示网桥都包括哪些虚拟网卡。
如何理解有用户角色的逻辑网络,它与网络标签的关系是什么?
使用网络标签可以大大简化一些逻辑网络管理的工作,这些管理任务包括创建与管理逻辑网络,以及将这些逻辑网络与物理主机网络接口绑定等。
网络标签就是一个可读性强的纯文本,它的长度没有限制,但它只能包括大小写字母、下划线和分号,但不支持空格和其它特殊字符。它可以与逻辑网络或物理主机网络接口进行关联。
为逻辑网络或主机的物理网络接口加一个网络标签后,它们就可以和有相同网络标签的逻辑网络或主机的物理网络接口以下面的形式相关联:
集群层的网络对象包括:集群、逻辑网络。常见的集群内网络拓扑图如下:
集群内的网络拓扑图
一个数据中心就是由多个集群组成的一个逻辑组,而一个集群则是由多个主机组成的一个逻辑组。上图中"集群内的网络拓扑图" 展示了一个集群中所包括的项。
在一个集群中的所有主机都可以访问相同的存储域,同时它们也在集群这一层来关联逻辑网络。对于一个虚拟机逻辑网络来说,为了使虚拟机可以使用它,它必须通过ovirt-engine在集群中的所有主机上定义并配置,而对于其它类型的逻辑网络,它们只需要在使用这些网络的主机上定义。
ovirt-engine中的数据中心支持多主机网络配置。当一个网络设置更新后,这个更新后的配置信息会自动应用到这个数据中心中所有分配了这个网络的主机上。
逻辑网络使得ovirt虚拟化环境可以根据网络数据类型对不同网络进行隔离。例如,在安装ovirt-engine时默认创建的ovirtmgmt网络则作为处理ovirt-engine和主机(VDSM)间的管理通信网络。一般情况下,具有类似要求和使用情况的多个网络通信可以组成一个逻辑网络。系统管理员通常会创建一个存储网络和一个显示网络来把这两种网络流量分离来,从而提高系统性能,以及便于故障排除。
逻辑网络的类型包括:
所有逻辑网络都可以设置为"必需的"或"可选的",二者选一。
逻辑网络在数据中心一级被定义,并被添加到主机上。为了使一个"必需的"逻辑网络可以正常工作,它需要被添加到相应集群中的所有主机上。
ovirt虚拟化环境中的每个虚拟机逻辑网络都必须由一个主机上的一个网桥设备支持。当为一个集群定义了一个新的虚拟机逻辑网络后,在这个集群的所有主机上都需要创建一个匹配的网桥设备,这样,这个新的虚拟机逻辑网络才可以被虚拟机使用。ovirt-engine会自动为虚拟机逻辑网络创建所需要的网桥设备。
ovirt-engine为虚拟机逻辑网络所创建的网桥设备需要和主机上的一个网络接口关联。如果和网桥相关联的主机网络接口已经被其它网络所使用,则新加入的网络接口将同样可以共享那些已经连接到主机网络接口中的网络。当虚拟机被创建并被添加到一个特定的逻辑网中时,它们的虚拟网卡会被添加到那个逻辑网所在的网桥中。这样,虚拟机就可以和连接到相同网桥中的其它设备进行网络通信。
不处理虚拟机网络流量的逻辑网络会和主机的网卡直接进行关联。
典型的逻辑网络拓扑如下图:
典型的逻辑网络拓扑图
一个"Required"网络就是一个逻辑网络,它对一个集群中的所有主机都有效。当一个主机上的"Required"网络无法工作时,在它上面运行的虚拟机会被迁移到另外一个主机上,而具体的迁移过程取决于所选择的调度策略。这一点对于运行关键任务服务的虚拟机非常重要,其所依赖的逻辑网络必须设置为"Required"。
请注意,当创建一个逻辑网络创建并加入到集群后,网络类型默认为:"Required"。
除了"Required"网络外,剩下的就是"Optional"网络。Optional网络可以只在需要它的主机上有效。有或没有可选的网络不会影响到一个主机的"Operational"状态。当一个Optional网络无法工作时,使用它的虚拟机不会被迁移到另一个主机上。
虚拟机网络是那些只处理虚拟机网络流量的逻辑网络。虚拟机网络可以是Required,也可以是Optional。使用一个Optional虚拟机网络的虚拟机只会在带有这个网络的主机上启动。
端口镜像会把指定逻辑网络和主机上的第3层网络流量复制到一个虚拟机的虚拟网络接口上。这样,通过这个虚拟机就可以进行网络纠错、网络优化、网络入侵检测以及对在同一个主机和逻辑网络中运行的虚拟机进行监控。
端口镜像只复制一个主机和一个逻辑网络内部的网络数据,它不会增加这个主机以外的网络流量。但是,启用了端口镜像功能的主机会比其它主机消耗更多CPU和内存资源。
端口镜像可以通过逻辑网络的vNIC配置集被启用或禁用,但它具有以下的限制:
鉴于以上限制,我们经常使用一个额外的专用的vNIC配置集,然后在其上启用端口镜像功能。
主机上常见网络配置类型包括:
在ovirt虚拟化环境中最简单的配置为"网桥 + 网卡"的配置,如下图。它通过网桥将一个或多个虚拟机连接至主机的物理网卡。
网桥与网卡配置
这种配置的一个实例就是安装ovirt-engine时自动创建的ovirtmgmt网桥。在安装的过程中,ovirt-engine在主机上安装 VDSM,VDSM的安装过程中会创建ovirtmgmt网桥。ovirtmgmt网桥可以获得主机的IP地址来实现主机的管理网络功能。
下图展示了基于VLAN的另一种可选配置:
基于VLAN的网桥与网卡配置
通过使用VLAN为这个网络中的数据传输提供了一个安全的通道。另外,使用多个VLAN还可以实现把多个网桥连接到一个网卡的功能。
下图所显示的配置中包括一个网络Bond,多个主机网卡通过Bond和同一个网桥和网络进行连接。
基于Bond的网桥与网卡配置
通过Bond创建了一个逻辑连接来把两个(或更多)物理网卡(NIC)连接起来。这种配置可以提供网卡容错、增加网络带宽等特性,具体取决于所配置的bond-mode。
下图所显示的配置把一个网卡连接到两个VLAN(这需要网络交换机的配合,相应的上联口需要配置为trunk模式)。主机使用两个独立的vNIC来分别处理两个VLAN网络数据,而每个VLAN会通过vNIC连接到不同的网桥中。在此基础上,每个网桥可以被多个虚拟机使用。
多网桥+多VLAN+单网卡配置
下图所显示的使用多个网卡组成一个Bond设备来连接到多个VLAN的配置视图。
多网桥+多VLAN+单Bond配置
这个配置中的VLAN通过Bond设备和网卡进行连接。每个VLAN连接到不同的网桥,每个网桥可以连接一个或多个虚拟机。
当设置了电源管理和隔离(fence)功能后,ovirt虚拟化环境将会提供更好的灵活性和稳定性。电源管理功能将可以使ovirt-engine控制主机的电源操作,其中最重要的一点是可以在主机出现故障时重新启动主机。
隔离功能主要用于把出现故障的主机从ovirt虚拟化环境中分离出来,从而使整个虚拟化环境正常运行。当被隔离的主机的故障被排除后,它可以重新返回正常工作的状态,并被重新加入到原来的虚拟环境中。
电源管理和隔离功能需要使用独立于操作系统的专用硬件来实现。ovirt-engine使用电源管理设备的IP地址或主机名来访问它。在ovirt虚拟化环境中,电源管理设备和隔离设备是相同的。
ovirt-engine不直接和隔离设备进行通讯,它使用一个代理来向主机的电源管理设备发送电源管理命令。ovirt-engine需要使用VDSM来操作电源管理设备,因此环境中还需要另外一个主机作为隔离代理。
我们可以选择:
隔离代理主机的状态有两种:Up和Maintenance。
ovirt-engine可以重启那些处于"无法正常工作(non-operational)"或"无响应(non-responsive)"状态的主机;或为省电关闭那些有低利用率的主机,但这些操作需要电源管理设备被正确配置。
ovirt虚拟化环境支持以下电源管理设备:
apc隔离代理不支持APC 5.x电源管理设备,我们需要使用apc_snmp隔离代理。
ovirt-engine使用隔离代理(fence agent)来和电源管理设备进行交流,系统管理员可以使用ovirt-engine配置电源管理的隔离代理(使用电源管理设备支持的参数)。对于简单的配置项可以通过系统提供的图形用户界面进行,而特殊的配置选项(只适用于特定隔离设备的选项)也可以通过这个界面被输入,但系统不会对它们进行任何处理,而直接把这些配置选项传递给隔离设备。
所有电源管理设备所支持的配置操作包括:
作为一个最佳的实践规则,我们需要在初始化配置完成后马上测试电源管理功能,并定期对运行环境中的电源管理功能进行测试。
一个稳定的系统需要在环境中的所有主机上正确地配置电源管理设备。在出现问题的主机上,ovirt-engine可以使用隔离代理跳过操作系统来直接和主机上的电源管理设备直接进行交流,并通过重启主机来把有问题的主机从虚拟化环境中隔离。如果被隔离的主机上有运行着开启高可用特性的虚拟机,ovirt-engine可以安全地把这些高可用性虚拟机迁移到其它主机上;如果被隔离的主机是SPM,ovirt-engine可以把SPM角色分配给其它主机。
在ovirt虚拟化环境中,隔离操作(fencing)就是由ovirt-engine通过使用隔离代理发起的、由电源管理设备负责执行的主机重启操作。隔离操作可以使集群对意料外的主机故障做出相应的响应;或根据预先设定的规则实现省电、负载均衡、虚拟机可用性策略等功能。
隔离功能可以保证SPM角色只属于一个正常工作的主机。如果被隔离的主机是SPM,这个SPM角色会被系统收回并分配给另外一台正常工作的主机。因为拥有SPM角色的主机是唯一一个可以修改数据域结构元数据的主机,所以如果作为SPM的主机没有配置隔离功能,当它出现故障时,环境中的所有需要修改数据域元数据的操作(如创建和销毁虚拟磁盘、进行快照、扩展逻辑卷等)都将无法进行。
当一个主机处于"无响应"状态时,在它上面运行的所有虚拟机也会处于"无响应"状态,而虚拟机对虚拟磁盘镜像操作所留下的"锁定"记录仍然会保留在主机上。这时,如果没有使用隔离功能,而直接在其它主机上重启那些无响应的虚拟机,并且对虚拟机有写操作权限时,虚拟机原来的磁盘镜像中的数据可能会被破坏。
使用隔离功能可以避免这个问题的出现。当主机被重启后,以前的"锁定"记录会被释放。ovirt-engine会使用一个隔离代理来确认出现问题的主机是否已经被重启。当ovirt-engine收到了主机已经重启成功的确认后,就可以在其它主机上重启这些虚拟机(它们之前运行在出现问题被隔离的主机上),而不会造成数据的破坏。隔离是实现高可用性虚拟机的基础,没有这个功能,高可用性虚拟机将无法在其它主机上运行。
当一个主机无响应时,ovirt-engine会等待30秒,然后再决定是否进行其它操作,这可以避免因为主机的临时性错误所可能引起的不必要的隔离操作。当等待超时后,主机仍然没有响应,ovirt-engine就会自动启动隔离操作。ovirt-engine使用电源管理设备的隔离代理来停止主机的运行;在确认主机已经停止后,再次启动主机,并确认主机已经被成功启动。当主机启动完成后,它会尝试重新加入到原来的集群中。如果主机的故障在启动后已被解决,它的状态会变为Up,并可以继续正常运行虚拟机。
有些时候,一个主机会因为无法预见的问题造成它处于无响应状态。此时尽管VDSM对所做出的请求无法响应,但依赖于VDSM的虚拟机仍然可以被访问。在这种情况下,重新启动VDSM就可能解决这个问题。
"SSH Soft Fencing"是ovirt-engine试图通过SSH在一个没有响应的主机上重启VDSM的过程。如果ovirt-engine无法通过SSH重启VDSM,而且配置了外部的隔离代理,则隔离操作将由外部的隔离代理进行处理。
要使用soft-fencing over SSH功能,主机必须配置并启用了隔离,一个有效的代理主机(数据中心中的另外一个主机,它的状态是UP)必须存在。当ovirt-engine和主机的连接出现超时情况时,会发生以下事件:
TimeoutToResetVdsInSeconds(默认值是 60 秒)+ [DelayResetPerVmInSeconds(默认值是 0.5 秒)]*(在主机上运行的虚拟机的数量)+ [DelayResetForSpmInSeconds(默认值是 20 秒)] * 1(如果主机是 SPM)或 0(如果主机不是 SPM)。
为了留给VDSM尽量多的超时等待时间,ovirt-engine会选择以上两个操作所需的最长时间。
vdsm restart命令。vdsm restart命令无法执行或执行失败,主机的状态将会变为NonResponsive,如果配置了电源管理,外部的隔离代理将会进行相应的硬件隔离操作。Soft-fencing over SSH可以在没有配置电源管理的主机上运行。这和通用的硬件隔离(fencing)有所不同:常见的硬件隔离只能在配置了电源管理的主机上运行。
单一的隔离代理都会被看做为"主要的"代理。"次要的"隔离代理只有在有第二个代理存在时才有效,而多个代理可以是同一个类型,也可以是不同的类型。
如果一个主机上只有一个隔离代理,当这个代理出现问题时,主机在被手动重启前将会一直出于"无响应"的状态,而在它上面运行的虚拟机也将处于停止的状态。只有当主机被人工隔离后,这些虚拟机才会被迁移到另外的主机上运行。而如果一个主机上使用了多个隔离代理,当一个代理失败时,其它的代理就可以被使用,这样就可以提高隔离功能的稳定性。
当在主机上定义了两个代理时,这两个代理可以配置为并行(concurrent)或顺序(sequential):
与调度相关的源码都在:
<ovirt-engine源码库根目录>/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/scheduling这个包下面。
一个单独主机所具有的硬件资源总是有限的,同时这些硬件资源也会出现故障。要解决这些问题,我们可以把多个主机组成一个集群来,这样就可以在不同的主机间共享资源。ovirt虚拟化环境使用负载均衡策略、调度和迁移机制来协调主机资源,这样就可以应对各种资源需求的变化。ovirt-engine可以保证一个集群中的所有虚拟机不会都运行在同一个主机上;另外,如果集群中出现某个主机的利用率非常低的话,ovirt-engine还会把这个主机上面的所有虚拟机迁移到其它主机上,从而可以关闭这个低利用率的主机来达到省电节能的目的。
当发生以下三个事件时,系统会检查环境中的可用资源:
当有效资源出现变化时,ovirt-engine会根据集群的负载均衡策略来调度虚拟机迁移操作。下面会对负载均衡策略、调度和虚拟机迁移做详细的介绍。
负载均衡策略是针对集群设置的。集群由一个或多个可以带有不同硬件参数和可用内存的主机组成,ovirt-engine会根据集群的负载均衡策略来决定在集群中的哪个主机上运行虚拟机。另外,负载均衡策略还指定了ovirt-engine会在什么情况下把过高利用率主机上的虚拟机迁移到其它主机。
数据中心的每个集群每隔1分钟会进行一次负载均衡处理。它会根据系统管理员预先为集群设定的负载均衡策略来决定哪些主机处于过度利用的状态;哪些主机的资源没有被充分利用;哪些主机仍有足够的资源用来运行从其它主机上迁移过来的虚拟机。负载均衡策略选项包括:VM_Evenly_Distributed、Evenly_Distributed、Power_Saving、None、Cluster_Maintenance。
使用这个策略的集群会把需要运行的虚拟机根据数量平均运行在集群中的每个主机上。
系统管理员需要设置每个主机上最多可运行多少台虚拟机,如果某个主机上所运行的虚拟机数量超过了这个最大值,这个主机就被识别为过载(overload)。另外,系统管理员还需要设置一个参数来指定最高利用率主机上所运行的虚拟机数量和最低利用率主机上所运行的虚拟机数量间的最大差值。
以上的2个值决定了"虚拟机迁移阈值(threshold)"的范围。当集群中的每个主机上所运行的虚拟机数据都在这个阈值范围内时,系统认为集群处于"负载均衡"的状态。
除此之外,因为作为SPM的主机通常需要有较低的虚拟机负载,因此管理员还可以设置在SPM主机上能预留的虚拟机资源的数量,这个值决定了SPM主机可以比其它主机少运行多少个虚拟机。
当集群中的某个主机上所运行的虚拟机数量超过了配置的最大值(单机可运行的最大虚拟机数量),并且集群中有至少一个主机所运行的虚拟机数量低于"虚拟机迁移阈值"的下限时,这个主机上的一个虚拟机就会被迁移到有最低利用率的主机上。如果迁移一个虚拟机后集群还没有处于"负载均衡"状态,系统会重复以上过程来迁移另外一个虚拟机,直到这个集群达到了"负载均衡"状态。
Evenly_Distributed
Evenly_Distributed这个策略是指:ovirt-engine通过选择CPU的负载最低或可用内存量最大的主机来运行一个新的虚拟机的策略。具体选择哪个指标(CPU负载或内存可用量)及指标的值,则由该策略的配置参数决定。
另外系统还可以设置一个时间值参数,这个参数的意思是说集群中的主机的最大CPU负载或最低内存可用量能够持续的最大时间值。如果主机CPU或内存最在负载持续时间超过这个阈值,则ovirt-engine将发起虚拟迁移任务,这个主机上的一个虚拟机会被迁移到集群中的CPU或内存利用率最低的一个主机上。如果迁移完成后,原来的主机的CPU和内存利用率仍然高于所限定的值,则重复以上步骤迁移它上面的另外一台主机,直到主机的CPU和内存利用率降到所限制的范围之内。主机资源检测任务一分钟执行一次。
Evenly_Distributed策略与VM_Evenly_Distributed策略的区别是:前者基于CPU或内存利用率作为迁移的触发参考条件,而后者则以运行在主机上的虚拟机数量作为迁移的触发参考条件。
Power_Saving
这个策略会把新虚拟机运行在CPU和内存利用率最低的主机上。
系统管理员可以设置一个"最大服务级别"值,这个值代表了集群中的主机所允许的最大CPU和内存利用率,如果主机的CPU和内存利用率超过了这个值,整个环境的性能就会下降。
系统管理员还可以设置一个"最低服务级别"值,它代表了在主机的 CPU和内存利用率低于这个值时,从用电的角度来讲,继续运行这个主机将会被判断为是不符合"经济效益"的。
另外,系统管理员还需要为"最大服务级别"和"最低服务级别"设置一个时间值,它指定了主机的CPU和内存利用率低于"最低服务级别"值(或高于"最大服务级别"值)多长时间后,ovirt-engine才会对主机采取行动来解决这个不符合"经济效益"的问题。
当一个主机的CPU和内存利用率高于"最大服务级别"值,并且处于这个状态的时间超过了所设定的时间值时,这个主机上的一个虚拟机会被迁移到集群中的CPU和内存利用率最低的一个主机上。如果迁移完成后,原来主机的CPU和内存利用率仍然高于所限定的值,则重复以上步骤迁移它上面的另外一台主机,直到主机的CPU和内存利用率降到所限制的范围之内。
当一个主机的CPU和内存利用率低于"最低服务级别"值,并且处于这个状态的时间超过了所设定的时间值时,这个主机上的所有虚拟机会在"最大服务级别"条件许可的情况下迁移到集群中的其它主机上。然后,ovirt-engine会自动关闭这台主机。如果系统在以后的某个时间需要更多的主机资源时,这个被关闭的主机会被重新启动(配合电源管理实现)。
如果没有选择任何负载均衡策略,新的虚拟机会在集群中的一个有可用内存,而且CPU利用率最低的主机上运行。其中的CPU利用率是通过一个算法对虚拟CPU的数量和CPU使用情况进行计算后得出的。如果选择使用这个选项,系统只有在要运行新虚拟机时才进行主机选择。另外,在主机负载增加时,系统也不会自动迁移虚拟机。
如果需要进行虚拟机迁移,系统管理员需要决定虚拟机要被迁移到哪台主机。另外,还可以使用固定 (pinning)功能把虚拟机和某个主机相关联。使用固定功能可以防止虚拟机被自动迁移到其它主机。对于需要消耗大量硬件资源的环境,手工迁移是最好的选择。
这个策略会限制集群内的一些活动(任务)。在这种策略下:
功能示意图
具有高可用性(HA)虚拟机资源预留策略可以使ovirt-engine监控集群资源的使用情况,从而可以保证在需要的时候为高可以性虚拟机提供有效的资源。ovirt-engine可以把虚拟机标识为"高可用性",在需要的时候,这些虚拟机可以在其它主机上重启。在高可用性虚拟机资源预留功能被启用时,ovirt-engine会为高可用性虚拟机预留一些资源,以防在主机故障的情况下,这些高可用虚拟机需要进行迁移时,能保证集群中有足够的资源来完成迁移操作。
在ovirt虚拟化环境中,调度(scheduling)是指ovirt-engine在集群中选择一个主机作为新虚拟机或将要迁移的虚拟机的目标主机(宿主机)的过程。
作为一个可以运行新虚拟机或从其它主机上迁移来的虚拟机的主机,它需要有足够的空闲内存和CPU资源来运行这些虚拟机。需要注意的是虚拟机是无法在CPU过载的主机上启动的。默认情况下,主机的CPU利用率在5分钟内都维持在80%以上的话,该主机就会被标识为CPU过载。5分钟与80%这两个参数是可以在管理界面上进行修改的。如果有多个主机都满足这个要求,系统会根据集群负载均衡策略来选择一个主机。例如,如果使用"Evenly_Distributed"策略,ovirt-engine会选择有最低CPU利用率的主机。如果使用"Power_Saving"策略,在"最大服务级别"和"最低服务级别"范围内的、CPU利用率最低的主机会被选择。另外,SPM的状态也会影响主机的选择过程。一个非SPM主机比SPM主机有更大的概率被选中。例如,如果集群中有SPM主机和非SPM主机,第一个需要在集群中运行的虚拟机会在非SPM主机上运行。
ovirt-engine使用迁移来实现集群的负载均衡策略,当集群中的主机负载处于一定状态时(超过最大值或低于最小值),虚拟机迁移操作就会被执行,从而可以保证主机负载满足集群的负载均衡策略。另外,虚拟机迁移操作还可以被设置为当主机被隔离(fence)或被设置为维护模式时自动进行。当需要进行迁移时,ovirt-engine会首先迁移CPU利用率最低的虚拟机(CPU利用率是一个百分比值,在计算这个值时不会考虑内存和I/O的使用情况,除非I/O操作会影响到CPU利用率)。当有多个虚拟机的CPU利用率相同时,ovirt-engine将根据SQL查询结果的列表顺序来决定第一个被迁移的虚拟机。
在默认情况下,虚拟机迁移有以下限制:
快照就是一个存储功能,它允许管理员为一个虚拟机的操作系统、应用程序和数据在特定时间创建一个恢复点。快照会把虚拟机当前的磁盘镜像保存为一个COW卷,并可以在以后把虚拟机恢复到虚拟机创建快照时的状态。当快照创建完成后,一个新的COW层会在当前层上被创建,快照创建后的所有写操作都会在新的COW层上执行。
虚拟机的硬盘镜像实际上是由一个或多个卷组成的,而从虚拟机的角度来看,这些卷以一个单一的磁盘镜像形式呈现。
"COW卷"和"COW层"是同一个概念,它们可以被相互替代使用,但"层"更贴近于快照的本质。通过快照,管理员可以随时抛弃那些在创建快照后所做的变更,这和许多程序所提供的Undo操作类似。
快照包括 3 个主要操作:
其它未正进行克隆或迁移操作的虚拟机都可以执行快照操作,此时的虚拟机的状态可以是运行、暂停或停止状态。
当对一个虚拟机进行实时快照时,ovirt-engine会要求SPM主机创建一个新的卷来给虚拟机使用。当新卷创建好后,ovirt-engine调用VDSM来和运行该虚拟机的主机上的libvirt和qemu进行交互,要求后续对虚拟机的写操作都新卷上进行。如果虚拟机可以在新卷上进行写操作,则认为快照已经成功完成,虚拟机将不再在旧的卷中写数据。如果虚拟机无法在新卷上进行写操作,则认为快照操作失败,新的卷会被删除。
在虚拟机快照开始后,直到快照完成前,虚拟机需要对当前卷和新创建的卷都进行访问,因此这两个卷都需要允许进行读和写操作。
安装了带有静止(quiescing)功能的guest代理的虚拟机可以保证文件系统在不同快照间的一致性。已经创建的Red Hat Enterprise Linux虚拟机可以安装qemu-guest-agent,这样就可以在进行快照前启用静止功能。
如果在进行快照时,虚拟机有支持静止功能的guest代理,VDSM会使用libvirt和代理进行交互来准备快照。 在实际进行快照前,没有完成的写操作会被完成,然后文件系统会被"冻结"。当快照操作完成后,libvirt会把对虚拟机的写操作切换到新的卷上,文件系统被"解冻",对磁盘的写操作会被恢复。
所有的实时快照都会尝试使用静止功能。如果虚拟机因为没有安装支持静止功能的guest代理而造成快照失败,实时快照会重新运行,但不会使用use-quiescing标志(即不会再尝试使用quiescing特性)。当一个使用了静止功能的文件系统的虚拟机使用快照恢复到以前状态时,虚拟机在启动时不会对文件系统进行检查。如果被恢复的虚拟机上没有使用静止功能,在使用快照恢复系统后,启动虚拟机时就需要对文件系统进行检查。
在ovirt虚拟化环境中,第一次为一个虚拟机创建快照和以后为这个虚拟机创建后续快照的过程不同。虚拟机的第一个快照会保留镜像格式(QCOW2或RAW),它把当前存在的卷作为一个基础镜像(母镜像)。后续的快照只是一个附加的COW层,它只记录当前系统和前一个快照中的变化。
在ovirt虚拟化环境中,一个虚拟机通常使用RAW磁盘镜像(除非在创建时使用了"精简(thin)"镜像,或用户指定使用QCOW2格式)。下图所示,创建的快照会包括虚拟磁盘的镜像,它将作为后续快照的基本镜像。
第一次为VM创建快照
在第一个快照创建以后再创建的快照将只创建一个新的COW卷,这个卷包括了当前系统和前一个快照间的变化。每个新的COW层在开始时都只包括COW元数据,而后续使用和操作虚拟机所产生的变化数据会被添加到这个新的COW层中。如果虚拟机需要修改前一个COW层的数据时,相应的数据会从前一层中读出,并把变化的数据写到新的COW层中。虚拟机在定位数据时会以从最新到最老的顺序在各个COW中查找。
非第一次为VM创建快照
系统管理员可以通过预览以前创建的所有快照来决定把虚拟机恢复到什么状态。
管理员可以在一台虚拟机的所有快照列表中选择一个快照来查看它的内容。如下图"预览快照" 所示,每个快照都被保存为一个COW卷,当它被预览时,这个快照会被复制到一个新的预览层上。虚拟机将会和预览层进行交流,而不是直接访问实际的快照。
在管理员预览快照后,可以使用这个快照来把虚拟机恢复到快照创建时的状态。如果管理员使用快照进行恢复系统后,虚拟机将会被关联到预览层。
在快照预览完成后,管理员可以选择Undo来删除在预览过程中创建的预览层。这时虽然预览层会被删除,原来保存快照的层还会保留。
快照预览
当快照不再需要时,您可以删除它们。删除快照后,您将无法把虚拟机恢复到这些快照所包括的时间点上。删除快照并不一定会获得更多的可用存储空间,而这些快照的数据也不一定会被实际删除。例如,您的虚拟机有5个快照,如果您删除了第3个快照,第3个快照中的数据可能仍然会存在在系统中,因为第4和第5个快照可能会需要这些数据(只有第3个快照中已变更的数据才会被删除,已变更的数据存在于第4个快照或第5个快照)。一般情况下,删除快照通常可以提高虚拟机的性能。
快照删除
快照删除会被作为一个异步的块任务处理,VDSM会为虚拟机在恢复文件中维护一个操作记录,从而使此任务可以被跟踪,即使在进行操作期间VDSM被重启或虚拟机被关闭时任务也可以被跟踪。当操作开始后,正被删除的快照将不能被预览,或作为一个恢复点(即使删除操作失败或被中断)。
活跃的层(可读可写的COW层)被合并到它的上一层的这一个操作可被分为两个阶段:
ovirt虚拟化环境为管理员提供了两种便捷的虚拟机创建工具:模板 (template)和虚拟机池(pool)。管理员可以通过使用模板来快速创建虚拟机。这个新虚拟机会基于一个已经存在的、并己经被配置好的虚拟机来创建,从而省去了手工安装操作系统和配置系统的步骤。模板功能对于需要创建多个相似虚拟机的环境非常有用。例如,我们需要使用多个虚拟机作为web服务器。我们可以首先在一台虚拟机上安装操作系统、安装web服务器软件并配置系统。然后,基于这个已经配置好的虚拟机创建一个模板。当您需要创建更多虚拟机来作为web服务器时,就可以使用这个模板来创建它们。
要创建一个模板,管理员需要先创建一个虚拟机,在新虚拟机上安装所需的软件包并进行相关的配置。对于即将作为模板的虚拟机来讲,其上面安装与配置的软件、应用、服务是比较通用的,以它作为模板创建的虚拟机基本不需要做太多的配置变更。另外,管理员可以执行一个可选的(但推荐使用)的动作:泛化(generalization)。泛化是指删除那些只与特定系统相关的、在不同的系统上会使用不同值的信息,如系统的用户名、密钥、时区。泛化对定制的配置不会有影响。 Red Hat Enterprise Linux虚拟机使用sys-unconfig进行泛化;Windows虚拟机使用Sysprep进行泛化。
sys-unconfig:请参考https://linux.die.net/man/8/sys-unconfig
sys-prep:请参考Sysprep
当一个准备被用来作为模板的虚拟机配置完成后(如果需要,进行了泛化操作),并被停止运行后,管理员就可以基于这个虚拟机创建一个模板。在模板创建的过程中,模板所用的虚拟磁盘镜像会被复制生成一个只读的镜像。这个只读的镜像就会作为所有基于这个模板所创建的虚拟机的母镜像。换个角度来说,模 板就是一个带有相关虚拟机硬件配置的、自定义的磁盘镜像。通过模板所创建的虚拟机的硬件配置可以再次修改。例如,基于配置了1GB内存的模板所创建的虚拟机的内存可以被配置为2GB。但是,母镜像本身不能被修改,这是因为对模板所做的修改将被影响到所有基于它所创建的全部虚拟机。
当一个模板被创建后,我们就可以使用它来创建多个虚拟机了。使用模板创建虚拟机有两种方式:精简(thin)模式和克隆(Clone)模式,ovirt-engine直接调用VDSM提供的两个不同的接口来实现。
虚拟机池就是一组基于特定模板创建的虚拟机,这些虚拟机可以快速地提供给用户使用。对虚拟机池中的虚拟机的使用权限是在虚拟机池一级来设置的。如果一个用户有对某个虚拟机池的使用权限时,这个用户就有权限使用这个虚拟机池中的任何虚拟机。因为用户每次从虚拟机池中获得的虚拟机可能并不是同一台虚拟机,所以虚拟机池并不适用于用户需要在虚拟机上保存数据的情况。虚拟机池适用于用户把数据保存在中央存储设备中的情况;或不需要存储新数据的情况。当虚拟机池创建完成后,组成这个虚拟机池的虚拟机会被创建,并处于"关闭"状态。当用户需要虚拟机时,虚拟机就会被启动。
虚拟机池可以快速地为用户提供相同的虚拟机(一般是虚拟桌面系统)。当一个有权限使用虚拟机池中的虚拟机的用户请求使用虚拟机时,用户的请求会被放置在一个"请求队列"中,系统会根据用户所提交的请求在"请求队列"中的位置来为用户分配一个可用的虚拟机。
当前的ovirt-engine版本(>=4.2)中,虚拟机池支持有状态与无状态两种类型。
有状态的虚拟机池:它的有状态并不是指池中的虚拟机是与某一个具体用户有关联关系,而是指用户A对虚拟机1所做的操作数据将会保存,当用户B向池中申请虚拟机时,ovirt-engine是可能会将用户A曾经使用过的虚拟机1分配给用户B的,此时用户B将会看到用户A在虚拟机1上的操作结果。很少会使用这种有状态的虚拟机池。
无状态的虚拟机池:虚拟机池中的虚拟机不具有数据持久性,这意味着每次用户使用虚拟机池中的虚拟机时,这个虚拟机都处于它的基本状态,而用户上次使用虚拟机时对虚拟机所做的更改不会被保留。这类虚拟机池适用于用户的数据被存储在一个中央存储的情况。
虚拟机池是通过模板创建的。池中的每个虚拟机都共享一个只读磁盘镜像,并使用一个临时的可写镜像用于保存虚拟机操作过程中可能需要持久化的数据。无状态的虚拟机池中的虚拟机和其它虚拟机不同,用户在使用它们时所产生的数据变化会在关闭虚拟机时被删除。这意味着虚拟机池所使用的存储较小(它只需要和模板相同的空间,再加上一些用来临时存储用户使用数据的存储空间)。使用虚拟机池来提供虚拟机比为用户提供单独虚拟机要节省更多的存储空间。
]]>oVirt项目定义了一套标准规范,通过这套标准,一个源码工程就可以一种通用的方式定义其应该如何被构建、测试、部署与发行,而这个定义规则是独立于开发该源码工程所使用的编程语言及工具,本质上讲它是由oVirt制定的适用于CI/CD(持续集成/持续部署)的DSL(Domain Specific Language),这套规范简称为STDCI。
CI团队可以基于这套标准创建像mock_runner这一类通用的构建、测试、部署与发行工具,这些工具以统一的方式来处理所有的ovirt工程。
oVirt项目的持续集成系统(CI系统)就是使用这套标准以自动化的方式执行该项目下各个工程的构建、测试、部署、发行进程。
该规范的实现请参考:
https://github.com/oVirt/jenkins/tree/master/scripts
STDCI中的一个核心概念是"stage",State是指作用在变更过的源码上的各种操作,它代表了源码从开发人员初始创建到成为正式发布产品整个生命周期的各个阶段。在每个特定的stage会执行一系列与该stage相关的各种操作(命令),这些操作命令是由每个具体工程定义。
当前STDCI定义了如下几个常用的stage:
automation的目录,该目录存放着每个stage需要执行的脚本文件。STDCI配置文件名可以有如何几种:
1
2
3
4
stdci.yaml, automation.yaml, seaci.yaml, ovirtci.yaml
stdci.yml, automation.yml, seaci.yml, ovirtci.yml
.stdci.yaml, .automation.yaml, .seaci.yaml, .ovirtci.yaml
.stdci.yml, .automation.yml, .seaci.yml, .ovirtci.yml
默认情况下,在进行工程构建与测试时,STDCI引擎将从工程根目录下依次搜索配置文件,然后在每个特定的stage根据配置文件中设置的规则执行automation目录下的相应脚本。
目前有两种方式构建与测试符合STDCI规范的工程:
mock_runner.sh在本地执行,请参考:mock-runner.sh使用说明。本文不深入讲解STDCI的使用说明,您可直接参考:STDCI介绍文档
ovirt-devops全局流程图
Jenkins持续集成系统有一个核心概念:任务(也称为Job),我们通常所说的搭建Jenkins持续集成环境其实就是创建一系列的任务并指定触发任务执行的条件(事件),在某一指定的事件被Jenkins系统捕获后,它将执行相应的任务,这一过程就是一次集成。
oVirt项目基于Jenkins搭建其CI/CD平台(后面简称DevOps平台),整个DevOps平台由以下几个部分组成:
Infra-Puppet, oVirt-jenkins项目;持续集成主要包括构建、测试、部署、发行四个阶段,其中"部署"主要是指部署测试环境,"发行"是指将生成的RPM等归档文件上传至版本发行系统。oVirt开源项目下的所有子项目都是符合STDCI标准,所有项目的持续集成任务都是自动化生成的,但任务的触发有多种:
Jenkins-Job管理是指通过UI或JJB的方式在Jenkins上创建、更新、删除任务;而Jenkins-Job管理自动化是指借助JJB自动创建、更新、删除任务,而不需要在Jenkins WebUI上人工操作。
实现原理也相对简单:因为Jenkins-Job定义了一系列持续集成任务,因此也可以再增加一个特殊的任务,这个任务就是JenkinsJobDeploy,一旦该任务被执行,它就会从Jenkins-Job定义Git仓库下载最新的任务定义脚本并更新Jenkins系统上的任务。
Jenkins任务初始化流程
Jenkins任务管理自动化流程
oVirt每个子项目的持续集成流程并无特殊之处,都根据Jenkins上的任务配置步骤依次执行相关脚本:编译、单元测试、构建、部署测试环境、系统测试、发行。
需要强调的是:这些持续集成相关的脚本都是通过JJB生成的,且这些脚本最终是由STDCI引擎串联调度完成一次集成过程,而开发人员只需要编写STDCI每个阶段(stage)需要执行的脚本。
Python是oVirt持续集成方案的主力开发语言,核心的STDCI控制引擎、OST系统测试框架、与ansible相关的自动化部署脚本、Jenkins Job Builder都是用Python编写。
Groovy是基于JVM的脚本语言,像Gradle这样的构建工具便是用Groovy开发的,它也可以同Java互操作,同时它也经常被用于实现代码自动生成这类功能。而Jenkins持续集成平台则使用Groovy来编写pipeline脚本。
在介绍Foreman之前,我们先介绍下[Bare Metal Provisioning]这个术语(英语不好,暂且翻译为"裸机供应")。所谓Bare Metal Provisioning是指为一台服务器(物理机或虚拟机)安装指定的操作系统的过程。通常情况下,有两种办法为服务器安装操作系统:
Foreman就是这样的软件工具,类似的还有Razor, Cobbler, RackHD。当然Foreman除了"裸机供应"能力外,它还有提供了其它服务,如与Puppet, Chef配合实现自动化配置与管理,它还提供了一个可视化的管理平台。更详细的信息还请直接访问官网。
Foreman是用Ruby语言开发的。
Puppet是开源的系统配置管理工具,基于C/S的部署架构。是一个为实现数据中心自动化管理而设计的配置管理软件,它使用跨平台语言规范,管理配置文件、用户、软件包、系统服务等。Puppet开源项目创建之初主要是为了解决配置管理自动化的相关问题,该项目目前存在两种形态:
Puppet也是用Ruby语言开发的。
Ansilbe是一个部署一群远程主机的工具。远程的主机可以是远程虚拟机或物理机, 也可以是本地主机。
Ansible与Open Source Puppet一样,不具备"裸机供应"能力,其也需要与Foreman或ansible-os-autoinstall等工具配合使用。
Ansible最初是作为自动化工具(如PupPET和Chef)的轻量级替代而开发的;但是它是用Python编写的(而不是Ruby),并且采用了一个基于SSH的无代理的架构(即不是采用Puppet的C/S架构,在被管理节点上不需要安装任何特殊软件)。
Ansilbe通过SSH协议实现远程节点和管理节点之间的通信,但是SSH必须配置为公钥认证登录方式,而非密码认证。理论上说,只要管理员通过ssh登录到一台远程主机上能做的操作,Ansible都可以做到。包括:
总得来说,Ansible有如下特点:
1
2
3
4
5
6
7
8
1、部署简单,只需在主控端部署Ansible环境,被控端无需做任何操作;
2、默认使用SSH协议对设备进行管理;
3、有大量常规运维操作模块,可实现日常绝大部分操作。
4、配置简单、功能强大、扩展性强;
5、支持API及自定义模块,可通过Python轻松扩展;
6、通过Playbooks来定制强大的配置、状态管理;
7、轻量级,无需在客户端安装agent,更新时,只需在操作机上进行一次更新即可;
8、提供一个功能强大、操作性强的Web管理界面和REST API接口——AWX平台。
Ansible是用Python开发的,它目前已被Redhat收购,是自动化运维工具中认可度最高的。oVirt的很多自动化部署子项目都是用Ansible开发的。
Lago是一个虚拟测试环境框架,它可在服务器或普通PC上为各种测试用例构建虚拟环境。oVirt-System-Test项目使用Lago框架来搭建测试环境。具体可参考官方文档或Github库。
Lago使用Python开发
JJB全称为Jenkins Job Builder,它是由openstack-infra团队开发的,项目地址:Jenkins Job Builder Github,详细信息请直接参考文档。
JJB使用Python开发
oVirt的持续集成方案也是目前IaaS开源领域的主流方案,从基础设施、编译构建、测试、部署等几个方面都实现了自动化。其中值得我们借鉴的地方有:
为了实现自动化水平,整个方案的涉及的技术栈也比较多,这也带来了一定的学习成本。至少在自动化配置管理方面可以统一使用Ansible。
提示:为了能比较顺畅的阅读本文档,您需要具体基本的docker基础知识及常用命令的使用。如果您尚未听说过docker,则请先通过docker官方文档学习必要的知识。
volumn参数映射至windows本地目录时,docker中的链接文件在windows的文件系统下是无法生效的,详见这里 制作本镜像的主要目的:快速创建并启动一个近似于本地的ovirt-engine容器,它可以为有需要的开发人员(特别是基于windows平台)提供近似于本地的开发、测试、调试环境。
由于Windwos系统的特殊性,建议在Windows-10专业版或企业版(64位)系统上安装Docker for windows(直接基于Windows Hyper-V);
而之前的Windows版本则需要使用Docker Toolbox on Windows(需要借助Oracle Virtual Box)。
在Windwos上激活Hyper-V意味着无法同时运行Virtual Box与VMWare workstation等其他虚拟化平台
如果您想自己定制一个新镜像,您才需要使用本节介绍的内容。正常情况下,您可以忽略本节内容。
1
2
3
# 假设工作目录为:~/workspace/Dockerfiles,要创建的镜像名为:simiam/ovirt-engine-dev
cd ~/workspace/Dockerfiles/ovirt-engine
docker build -t simiam/ovirt-engine-dev ./latest
想快速搭建一个ovirt-engine开发调试环境的话,从这一节开始看起。
基于simiam/ovirt-engine-dev镜像创建一个名为ovirt-engine-dev的容器,容器的主机名为ovirt-engine-dev。
1
2
# Stop and remove 'ovirt-engine-dev' docker container IF necessary.
docker stop ovirt-engine-dev && docker rm ovirt-engine-dev
ovirt-engine-dev,后续会提供可配置的环境变量1
2
3
4
5
6
7
# 1. 以下命令的'\'符号只在linux或macos环境下生效,windows下需要遵循相应bat脚本语法,如果不懂建议写成一行
# 2. 与路径有关的参数值不能包含空格
docker run -d -i -t --name ovirt-engine-dev -h ovirt-engine-dev --privileged \
-v /env/.m2/:/env/.m2 -v ~/env/ovirt-engine:/env/workspace -v /env/ovirt-engine-deploy:/env/deploy \
-p 10022:22 -p 5432:5432 -p 8080:8080 -p 8787:8787 -p 8443:8443 /
simiam/ovirt-engine-dev /usr/sbin/init
再次强调一下:
在windows环境下,不能将/env/deploy目录映射至windows本地,即不需要
-v /env/ovirt-engine-deploy:/env/deploy这一部分。
参数详细说明如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
docker run -d -i -t # 通用参数,参考官网
--name ovirt-engine-dev # 容器名
-h ovirt-engine-dev # 容器中OS系统的hostname,如果使用默认的部署环境,则主机名必须为:ovirt-engine-dev
--privileged # 配合/usr/sbin/init参数,以开启特权模式(使systemctl可用)
-v /env/.m2/:/env/.m2 # maven本地仓库映射关系
-v ~/env/ovirt-engine:/env/workspace # 源码工程根目录映射关系,前者代表宿主机目录,即本地目录(下同)
-v /env/ovirt-engine-deploy:/env/deploy # 打包部署根目录映射关系 --------》》》》在windows环境下,不能将/env/deploy目录映射至windows本地
-p 10022:22 # SSH端口映射关系,10022为宿主机端口
-p 5432:5432 # Postgres数据库端口映射关系
-p 8080:8080 # ovirt-engine webadmin HTTP服务端口映射关系
-p 8787:8787 # 远程调试端口映射关系
-p 8443:8443 # ovirt-engine webadmin HTTPS服务端口映射关系(目录不可用)
simiam/ovirt-engine-dev # 镜像名
/usr/sbin/init # 容器启动时需要执行命令,与privileged配合使用
| 服务名 | 容器内部端口 | 宿主机端口 |
|---|---|---|
| SSH | 22 | 10022 |
| Postgresql | 5432 | 5432 |
| HTTP | 8080 | 8080 |
| HTTPS | 8443 | 8443 |
| 远程调试 | 8787 | 8787 |
容器内部端口目前是固定的,不支持根据环境变量来动态配置;宿主机端口则可根据个人喜好来配置。
docker stop ovirt-engine-devdocker start ovirt-engine-devdocker restart ovirt-engine-dev1
docker exec -it -u ssh_ovirt ovirt-engine-dev /env/start-engine-service.sh
ssh -p <映射端口号> ssh_ovirt@localhost,其中映射端口号为1.3节创建容器实例命令docker run ...中与容器内22号端口映射的宿主机端口号,如示例中的10022;SSH用户ssh_ovirt的密码:ssh_ovirt,root用户的密码:engine。 – 推荐docker exec -it <容器名> /bin/bashovirt-engine平台部署在wildfly(jboss)服务器上,但其并不是采用wildfly官方所描述的标准部署方式及部署结构,其使用一套自己的部署目录结构,如下:
Dir-Tree
这些目录与wildfly环境的关联关系
1
2
3
4
5
6
7
-Djboss.home.dir=/usr/share/ovirt-engine-wildfly
-Djboss.server.base.dir=/env/ovirt-engine-deploy/share/ovirt-engine
-Djboss.server.data.dir=/env/ovirt-engine-deploy/var/lib/ovirt-engine
-Djboss.server.log.dir=/env/ovirt-engine-deploy/var/log/ovirt-engine
-Djboss.server.config.dir=/env/ovirt-engine-deploy/var/lib/ovirt-engine/jboss_runtime/config
-Djboss.server.temp.dir=/env/ovirt-engine-deploy/var/lib/ovirt-engine/jboss_runtime/tmp
-Djboss.controller.temp.dir=/env/ovirt-engine-deploy/var/lib/ovirt-engine/jboss_runtime/tmp
工作目录统一用
$WORK_DIR表示
经过第1章节的介绍,相信您已经可以在本地使用ovirt-engine平台。本章节将介绍如何基于ovirt-engine容器进行本地开发与调试。
从零开始搭建ovirt-engine环境到部署成功(可以访问web)共包括4个步骤:编译 -> 打包(部署) -> 初始化 -> 启动,各个步骤使用的命令如下:
make clean install-dev PREFIX=/env/deploy BUILD_UT=0 DEV_EXTRA_BUILD_FLAGS="-Dgwt.compiler.localWorkers=1 -Djava.net.preferIPv4Stack=true",就可以生成类似1.7节介绍的部署目录结构及相关内容。/env/deploy/bin/engine-setup,初始化内容主要是配置ovirt-engine系统主要参数,执行数据库创建脚本等/env/deploy/share/ovirt-engine/services/ovirt-engine/ovirt-engine.py start,该命令是用来启动wildfly(jboss)服务器,从而启动ovirt-engine管理平台。注意:以上三个命令都需要在docker容器内执行。
其中,编译webadmin阶段会消耗大量内存,建议宿主机内存至少为8G(推荐12G)。
git clone https://github.com/ovirt/ovirt-engine.gitmvn clean install构建出相应的war,ear等部署文件。这一步是我们日常开发过程中经常使用到的:
一般情况下我们需要将生成的相关部署文件通过jboss-cli或jboss-web重新部署,或直接将变更文件覆盖部署目录下的相应文件并重启服务。
在2.2节中使用的mvn clean install只能在工程的target目录下产生相关的jar,war,ear等文件,并不会生成目录结构及其下的相关内容。
为了完成部署需要在docker环境下使用make install-dev ...命令进行一次完整的构建、部署。
因为这一步耗时比较长,因为基于simiam/ovirt-engine-dev镜像创建的容器已经事先替您执行了一次打包部署,所以一般情况下您不需要执行这一步操作,除非发生了以下几种情况:
如果没有出现诸如文件结构变更或数据库表结构等元数据变更的情况,就没有必要重新部署。只需要重新执行make install-dev <args>命令进行文件刷新(覆盖)即可。
如果数据库表结构等发生的变更,则需要重新执行engine-setup命令以更新数据库表结构。
为了避免一些莫名其妙的问题,在进行重新部署时(代码有重大变更)需要删除${PREFIX}所代表的目录,然后再构建、安装部署。
${PREFIX}/bin/engine-cleanup也是一个不错的环境清理工具。当部署目录的内容变更量比较少的时候,可以使用这个工具进行环境清理。
切记:以上操作均需要停止服务,再操作,最后重启engine服务
初始化内容主要是配置ovirt-engine系统主要参数,执行数据库创建脚本等。因此如果你想重新构建数据库、修改配置参数等,则可以执行初始化命令。
本节将详细介绍创建一个ovirt-engine容器实例所指定的各参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
docker run -d -i -t # 通用参数,参考官网
--name ovirt-engine-dev # 容器名
-h ovirt-engine-dev # 容器中OS系统的hostname,如果使用默认的部署环境,则主机名必须为:ovirt-engine-dev
--privileged # 配合/usr/sbin/init参数,以开启特权模式(使systemctl可用)
-v /env/.m2/:/env/.m2 # maven本地仓库映射关系
-v ~/env/ovirt-engine:/env/workspace # 源码工程根目录映射关系,前者代表宿主机目录,即本地目录(下同)
-v /env/ovirt-engine-deploy:/env/deploy # 打包部署根目录映射关系
-p 10022:22 # SSH端口映射关系,10022为宿主机端口
-p 5432:5432 # Postgres数据库端口映射关系
-p 8080:8080 # ovirt-engine webadmin HTTP服务端口映射关系
-p 8787:8787 # 远程调试端口映射关系
-p 8443:8443 # ovirt-engine webadmin HTTPS服务端口映射关系(目录不可用)
simiam/ovirt-engine-dev # 镜像名
/usr/sbin/init # 容器启动时需要执行命令,与privileged配合使用
如果您使用默认提供的部署环境,则可以容器已开放了8787远程调试端口,您通过宿主机的映射端口便可进行远程调试。
Just install the Docker CE that fit for your OS.
In order install Docker for windows(Based Windows Hyper-V), it requires Microsoft Windows 10 Professional or Enterprise 64-bit.
For previous versions get Docker Toolbox on Windows(Based Oracle Virtual Box).
1
2
cd <repository_root_dir>
docker build -t simiam/ovirt-engine-dev ./latest
1
2
3
4
5
6
7
8
9
# Stop and remove 'ovirt-engine-dev' docker container IF necessary.
docker stop ovirt-engine-dev && docker rm ovirt-engine-dev
# The container's hostname MUST BE 'ovirt-engine-dev'
docker run -d -i -t --name ovirt-engine-dev -h ovirt-engine-dev --privileged \
-v /env/.m2/:/env/.m2 -v ~/workspace/cloudos/ovirt/ovirt-engine:/env/workspace -v /env/ovirt-engine-deploy:/env/deploy \
-p 10022:22 -p 5432:5432 -p 8080:8080 -p 8787:8787 -p 8443:8443 /
simiam/ovirt-engine-dev /usr/sbin/init
1
2
3
docker start ovirt-engine-dev
docker exec -it -u ssh_ovirt ovirt-engine-dev /env/start-engine-service.sh
本文先简单介绍下与分布式系统相关的几个概念。
所谓分布式锁是指在一个分布式集群中,同一个方法在同一时间只能被一台机器上的一个线程执行,也就是所谓的分布式互斥。就像单机系统上的多线程程序需要用操作系统锁或数据库锁来互斥对共享资源的访问一样,分布式程序也需要通过分布式锁来互斥对共享资源的访问。分布式锁是保障数据一致性的手段之一。
一般情况下,我们可以使用数据库、Redis或者ZooKeeper来做分布式锁服务。不管怎么样,分布式的锁服务需要有以下几个特点:
1)安全性:在任意时刻,只有一个客户端可以获得锁(排他性)
2)避免死锁:客户端最终一定可以获得锁,即使锁住某个资源的客户端在释放锁之前崩溃或网络不可达
3)容错性:只要锁服务集群中的大部分节点存活,客户端就可以进行加锁解锁的操作
即分布式锁服务在实现上一般关注如下三个问题:
1)锁获取机制:超时释放导致多方获取同一把锁问题(CAS机制)
2)锁释放机制:正常释放、超时释放
3)客户端如何知道锁被释放:客户端不断重试、服务端主动通知
说起数据一致性,简单说有三种类型:
1)Weak(弱一致性):当你写入一个新值后,读操作在数据副本上可能读出来,也可能读不出来。比如:某些cache系统,网络游戏其它玩家的数据和你没什么关系。
2)Eventually(最终一致性):当你写入一个新值后,有可能读不出来,但在某个时间窗口之后保证最终能读出来。比如:DNS,电子邮件、Amazon S3,Google搜索引擎这样的系统。
3)Strong(强一致性):新的数据一旦写入,在任意副本任意时刻都能读到新值。比如:文件系统,RDBMS,Azure Table都是强一致性的。
从这三种一致型的模型上来说,我们可以看到,Weak和Eventually一般来说是异步冗余的,而Strong一般来说是同步冗余的,异步的通常意味着更好的性能,但也意味着更复杂的状态控制。同步意味着简单,但也意味着性能下降。
单纯讲理论比较枯燥,本节我们会结合例子来描述。
让我们用最经典的Use Case:"A帐号向B帐号汇钱"来说明一下,熟悉RDBMS事务的都知道从帐号A到帐号B需要6个操作:
1)从A帐号中把余额读出来。
2)对A帐号做减法操作。
3)把结果写回A帐号中。
4)从B帐号中把余额读出来。
5)对B帐号做加法操作。
6)把结果写回B帐号中。
为了数据的一致性,这6件事,要么都成功做完,要么都不成功,而且这个操作的过程中,对A、B帐号的其它访问必需锁死,所谓锁死就是要排除其它的读写操作,不然会有脏数据的问题,这就是分布式事务。
目前业界用于实现分布式事务的方案有:
1)2PC(两阶段提交):可以数据强一致性,但存在性能问题、协调过程中TimeOut问题(协调者可用性问题)
2)3PC(三阶段提交):其核心理念是:在询问的时候并不锁定资源,除非所有人都同意了,才开始锁资源。
3)事务补偿机制:并行处理一个事务的多个阶段,然后根据不同阶段的执行结果进行相应的业务调整(也称事务补偿),其通常是基于工作流引擎来实现,只保证数据最终一致性,可实现高性能。如电商的秒杀功能经常这么设计:下单成功与订单确认机制。
可进一步学习数据库事务ACID属性的变种:BASE(Basic Availability 基本可用,Soft state 软状态,Eventual Consistency 最终一致性)以及基于BASE的事务补偿。
当我们在生产线上用一台服务器来提供数据服务的时候,会遇到如下的两个问题:
1)一台服务器的性能不足以提供足够的能力服务于所有的网络请求。
2)我们总是害怕我们的这台服务器停机,造成服务不可用或是数据丢失。
于是我们不得不对我们的服务器进行扩展,加入更多的机器来分担性能上的问题,以及来解决单点故障问题。 通常,我们会通过两种手段来扩展我们的数据服务:
1)数据分区:就是把数据分块放在不同的服务器上。
2)数据镜像:让所有的服务器都有相同的数据,提供相当的服务。
对于第一种情况,我们无法解决数据丢失的问题,单台服务器出问题时,会有部分数据丢失。所以,数据服务的高可用性只能通过第二种方法来完成——数据的冗余存储(一般工业界认为比较安全的备份数应该是3份)。 但是,加入更多的机器,会让我们的数据服务变得很复杂,尤其是跨服务器的事务处理,也就是跨服务器的数据一致性。这个是一个很难的问题。
那么,我们在加入了更多的机器后,这个事情会变得复杂起来:
1)在数据分区的方案中:如果A帐号和B帐号的数据不在同一台服务器上怎么办?我们需要一个跨机器的事务处理。也就是说,如果A的扣钱成功了,但B的加钱不成功,我们还要把A的操作给回滚回去。
2)在数据镜像的方案中:A帐号和B帐号间的汇款是可以在一台机器上完成的,但是别忘了我们有多台机器存在A帐号和B帐号的副本。如果对A帐号的汇钱有两个并发操作(要汇给B和C),这两个操作发生在不同的两台服务器上怎么办?也就是说,在数据镜像中,在不同的服务器上对同一个数据的写操作怎么保证其一致性,保证数据不冲突?此时分布式锁也许就可以派上用场了。
对于分布式系统,除了上面的可用性、数据一致性,我们还要考虑性能的因素,如果不考虑性能的话,事务得到保证并不困难,系统慢一点就行了。除了考虑性能外,我们还要考虑可用性,也就是说,一台机器没了,数据不丢失,服务可由别的机器继续提供。 于是,我们需要重点考虑下面的这么几个情况:
1)容灾:数据不丢、结点的Failover
2)数据的一致性:事务处理(锁)
3)性能:吞吐量 、 响应时间
前面说过,当出现某个节点的数据丢失时可以从副本读到,数据副本是分布式系统解决数据丢失异常的唯一手段。为简单起见,我们只讨论在**数据冗余(数据镜像)**方案下考虑数据的一致性和性能的问题。简单说来:
1)要想让数据有高可用性,就得写多份数据。
2)写多份的问题会导致数据一致性的问题。
3)数据一致性的问题又会引发性能问题
我们似乎看到了分布式场景下的CAP理论的影子(有兴趣的自行深入研究,这里不再展开)。
所谓"状态",是指程序运行中的一些数据或是程序运行上下文。比如用户每一次请求在服务端所保留下来的数据(记录),像用户登录时的Session,我们需要使用这个Session来判断这个请求的合法性;还有一个业务流程中需要让多个服务组合起来形成一个业务逻辑的运行上下文Context,这些都是状态。
一直以来,无状态的服务被当成分布式服务设计的最佳实践和铁律。因为无状态的服务对于扩展性和运维实在是太方便了,没有状态的服务可以随意地增加和减少节点,可以随意的搬迁,而且可以大幅度降低代码的复杂度。
但是现实世界是一定会有状态的,这些状态可能表现在如下几个方面:
为了做出无状态的服务,我们通常需要把状态保存到一个第三方的地方,比如Redis,ZooKeeper/Etcd这样的高可用存储中。从另一角度讲,为了实现无状态服务会导致应用服务依赖于第三方有状态的存储服务,同时也增加了网络开销,会增加服务的响应时间。
在互联网领域,有状态的服务看上去比较"反动"。因为无状态服务需要把状态存放在第三方存储上,这样便增加了网络开销,为了减少网络开销有时就引入本地数据缓存,此时如果未引入Sticky Session机制,用户的每次请求并不一定会路由至同一台机器,结果将导致所有机器上都会创建相同的数据缓存,这也算是一种资源浪费。而如果引入了Sticky Session机制便可解决该资源浪费问题。
所谓Sticky Session,就是对于客户端传来的请求,都能保证其落在同一台机器上,相当是数据分片。这样我们完全不需要考虑数据会被加载到不同的节点,这样的架构模型就变简单了。通过一致性哈希便可以实现Sticky Session,但只是简单的使用一致性哈希会导致负载与数据不均匀(哈希环平衡又是另一个话题了,本文不深入讨论,有需要的请求参考这里)。
水平扩展也叫横向扩展,是指为分布式应用集群添加一个新的服务节点来提升该分布式应用的处理能力。一般是在单机节点性能已无法进一步优化的情况下才会进一步考虑水平扩展。
一个可水平扩展的应用系统,其架构设计一般都要考虑以下几点:
1)使用本地锁:当前Engine代码中大量使用JDK提供的锁机制来保证对同一个VDSM的并发请求,最典型的就是InMemoryLockManager提供的锁操作,其需要扩展成支持分布式锁。
VDSM是否支持并发请求?
2)有状态服务及认证会话:Engine中使用的EJB、部分服务实例(如ResourceManager)是有状态的服务。
3)定时计划任务:Engine中的定时任务也需要调整,否则会导致每个engine节点在启动后都执行相同的计划任务。
4)数据库:数据库服务目前未提供HA服务(这部分可独立考虑,个人认为不需要在本方案中考虑)
5)配置文件:配置文件也需要在集群成员间同步,目前版本的engine也不支持。
为了在分布式环境下实现VM等资源互斥访问需求,有两种方案:
1)基于一致性哈希来保证每次请求都落在同一个engine节点
2)引入分布式锁:Redis、ZooKeeper或基于infinispan自行实现
方案1:实现起来相对简单,只需要在每个engine中实现hash redirect模块,但若出现节点数量变更重建hash环的过程中仍然无法保证资源互斥问题。
方案2:性能会有所损失,同时改造量比较大,需要找出现有代码中所有本地锁并调整为分布式锁;
由于我们云管平台的使用场景并非像互联网产品会频繁的变更engine节点,因此重建hash环的概率相当低,同时VDSM本身也有并发保护机制,因此优先考虑使用方案1,后续随着对engine代码的深入研究再进一步考虑方案2。
同样的两种方案:
1)基于一致性哈希来保证每次请求都落在同一个engine节点
2)分布式缓存
方案1:与3.1节的需求不同,节点宕机导致哈希环重建会导致缓存数据迁移或丢失,该方案容错性太差,因此不能够用它来保障业务。(我们引入一致性哈希只是为了实现资源互斥而已,这点要搞清楚。)
方案2:由于engine是运行在wildfly服务器中,而wildfly原生提供了分布式缓存服务infinispan,同时其对Session复制也提供了现成的支持,在技术上不存在障碍。主要还是工作量问题,即需要识别出各个有状态的服务如ResourceManager,没有其他捷径。
由于engine的计划任务是基于quartz,而quartz是支持分布式场景的,因此这部分也不存在技术问题。
可选择的方案:
1)使用基础平台提供的文件同步方案
2)基于ZooKeeper或Etcd开源方案实现文件同步(该方法对部署有要求,最少是三个节点)
优先使用方案1,如果基础平台未提供则采用方案2。
总得来说,ovirt-engine的水平扩展方案在技术上是可行的且不存在技术障碍,主要还是源码分析的工作量问题。
正如Jack Reecves所发表的《源码就是设计》:源码就是最好软件设计文档,而其他非代码性的文档只是源码的辅助。本文并非为了讨论编程与软件设计的关系,只想借以说明源码的重要性。
简单讲,整洁代码行云流水如同阅读精美好文,代码能够尽可能的自解释;具体讲,整洁代码具备如下特性:
我们写文章都是先构思再下笔,但一般不可能一气呵成。都是先出初稿再调整、使用精美词句慢慢打磨。写代码同写文章一样,也是一个不断调整修饰的过程(修改名称、分解函数、消除重复等操作不断重复)。只要你有一颗写出整洁代码的初心。
} // while]]>转载请注明出处:cloudnoter.com
https://github.com/monkeychen/xspring
xspring是个组件集,后续会不断增加新的通用组件,本文所介绍的动态多数据源组件位于xspring项目的xspring-data模块中,其maven坐标如下(尚未上传至maven中央库):
1
2
3
4
5
<dependency>
<groupId>org.xspring</groupId>
<artifactId>xspring-data</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
本框架是在spring框架提供的各种特性的基础上进行开发的,具体如下:
AbstractRoutingDataSourcecore组件是所有其他组件的基础,其提供了通用的事件总线、日志管理等功能。其中XspringApplication是整个框架的启动类,通过调用这个启动类的startup静态方法,并提供您打上@org.springframework.context.annotation.Configuration注解的启动类来启动您的应用。
框架启动时按如下顺序加载配置文件
- file:./config/env.properties
- classpath:/config/env.properties
- file:./config/xspring.properties
- file:./xspring.properties
- classpath:./config/xspring.properties
- classpath:./xspring.properties
上述配置文件中env.properties文件中的属性信息支持热加载(即每隔指定时间框架都会读取该文件并更新至SystemProperties),相关源码请参考类EnvironmentInitializer.
基于Xspring框架的应用启动示例代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@ImportResource("classpath:/config/customized-beans.xml") // 可以通过ImportResource方式加载定义在XML中的bean信息。
public class DemoApplication {
private ApplicationContext applicationContext;
private DemoService demoService;
public static void main(String[] args) throws Exception {
applicationContext = XspringApplication.startup(DemoApplication.class, args);
demoService = applicationContext.getBean("demoService", DemoService.class);
}
}
XspringApplication类的startup方法会创建XspringApplication实例并调用其run方法:
1
2
3
public static ConfigurableApplicationContext startup(Class<?> annotatedClass, String[] args) {
return new XspringApplication().run(annotatedClass, args);
}
在run方法中,框架会通过SPI方式加载其他组件提供的标注有@Configuration注解的org.xspring.core.extension.ModuleConfiguration接口实现类,从而启动其他组件。
SPI声明统一存放在各个组件的classpath:/META-INF/spring.factories文件中,整个加载过程源码如下:
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
public ConfigurableApplicationContext run(Class<?> annotatedClass, String[] args) {
logger.debug("The input arguments is:{}, {}", annotatedClass, args);
AnnotationConfigApplicationContext context = null;
// Load other configurations in spring.factories file
ClassLoader classLoader = ClassUtils.getDefaultClassLoader();
List<String> factoryNames = SpringFactoriesLoader.loadFactoryNames(ModuleConfiguration.class, classLoader);
List<Class> moduleConfigClasses = Lists.newArrayList();
moduleConfigClasses.add(XspringConfiguration.class);
if (CollectionUtils.isNotEmpty(factoryNames)) {
for (String factoryName : factoryNames) {
try {
Class factoryClass = ClassUtils.forName(factoryName, classLoader);
moduleConfigClasses.add(factoryClass);
} catch (ClassNotFoundException e) {
logger.warn("Can not find the matched class[{}] in classpath!", factoryName);
}
}
}
if (annotatedClass != null) {
moduleConfigClasses.add(annotatedClass);
}
Class[] configurations = moduleConfigClasses.toArray(new Class[moduleConfigClasses.size()]);
context = new AnnotationConfigApplicationContext(configurations);
context.start();
return context;
}
xspring-core组件还提供了一个基于Google Guava库的事件总线模型,有兴趣的朋友可直接参考
org.xspring.core.eventbus包下的相关源码。
xspring-data组件的模块定义(启动)类为:XspringDataConfiguration,其会通过@Import(DataSourceInitializer.class)方式加载动态数据源初始化配置类。DataSourceInitializer也是一个@Configurable注解类,其通过@Bean的方式定义了datasource这个动态数据源。
在动态数据源bean创建过程中,组件会通过SPI方式加载DataSourceFactory这个接口的实现类来获取具体的数据库连接池提供方,框架默认提供了DruidDataSourceFactory。
您也可以自己提供数据库连接池的实现类(同样定义在各个组件的classpath:/META-INF/spring.factories文件中),并通过添加org.springframework.core.annotation.Order注解来指定加载优先级。
DataSourceInitializer会从属性文件datasource.properties中加载JDBC配置信息,然后通过DataSourceFactory实现类创建相关的数据库连接池(真正的数据源)实例。datasource.properties文件加载位置如下(先后顺序):
- file:./config/datasource.properties
- classpath:/config/datasource.properties
框架默认提供的Druid数据库连接池所使用的datasource.properties文件内容大致如下(不同的数据库连接池提供方则会有所差别):
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
# 数据源个数
xspring.datasource.jdbc.max=2
# 默认数据源编号
xspring.datasource.jdbc.default=1
# 第一个数据源
xspring.datasource.jdbc.1.name=ds_mysql
xspring.datasource.jdbc.1.driverClassName=com.mysql.jdbc.Driver
xspring.datasource.jdbc.1.url=jdbc:mysql://localhost:3306/mooc?useUnicode=true&characterEncoding=UTF8
xspring.datasource.jdbc.1.username=root
xspring.datasource.jdbc.1.password=*****
xspring.datasource.jdbc.1.initialSize=10
xspring.datasource.jdbc.1.minPoolSize=5
xspring.datasource.jdbc.1.maxPoolSize=10
xspring.datasource.jdbc.1.maxWait=60000
xspring.datasource.jdbc.1.poolFilters=stat
# 第二个数据源
xspring.datasource.jdbc.2.name=ds_postgresql
xspring.datasource.jdbc.2.driverClassName=org.postgresql.Driver
xspring.datasource.jdbc.2.url=jdbc:postgresql://localhost:5432/blog
xspring.datasource.jdbc.2.username=postgres
xspring.datasource.jdbc.2.password=*****
xspring.datasource.jdbc.2.initialSize=10
xspring.datasource.jdbc.2.minPoolSize=5
xspring.datasource.jdbc.2.maxPoolSize=10
xspring.datasource.jdbc.2.maxWait=60000
xspring.datasource.jdbc.2.poolFilters=stat
注意:xspring-data组件的动态多数据源目前只支持相同数据库连接池提供方,即只会使用order值最小的
DataSourceFactory实现类来创建真正的数据源对象。
源码如下:
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
@Bean
public DataSource dataSource() {
// 加载DataSourceFactory实现类列表,返回的实例已根据@order注解进行升序排序
ClassLoader classLoader = ClassUtils.getDefaultClassLoader();
// 通过SPI方式加载DataSourceFactory这个接口的实现类来获取具体的数据库连接池提供方,框架默认提供了DruidDataSourceFactory。
List<DataSourceFactory> dataSourceFactories = SpringFactoriesLoader.loadFactories(DataSourceFactory.class, classLoader);
if (CollectionUtils.isEmpty(dataSourceFactories)) {
throw new BeanCreationException("Not found any DataSourceFactory implementer in class path!");
}
DataSourceFactory dataSourceFactory = dataSourceFactories.get(0);
Map<String, DataSource> dataSourceMap = dataSourceFactory.loadOriginalDataSources(environment);
if (CollectionUtils.isEmpty(dataSourceMap)) {
throw new BeanCreationException("Fail to load any original DataSource instance!");
}
Map<Object, Object> targetDataSourceMap = Maps.newHashMap();
dataSourceMap.forEach((name, dataSource) -> {
beanFactory.registerSingleton(name, dataSource); // 将具体的DataSource实例注册进ApplicationContext
targetDataSourceMap.put(name, dataSource);
DynamicDataSourceContextHolder.addDataSourceId(name);
});
DataSource defaultDataSource = dataSourceFactory.getDefaultDataSource(environment);
DynamicDataSource dataSource = new DynamicDataSource();
dataSource.setTargetDataSources(targetDataSourceMap);
dataSource.setDefaultTargetDataSource(defaultDataSource);
return dataSource;
}
使用示例见单元测试类:DynamicDataSourceTest, DemoServiceImpl
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
// DemoServiceImpl源码:
@Component("demoService")
public class DemoServiceImpl implements DemoService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
@TargetDataSource("ds_mysql")
public void printClassroomList() {
List list = jdbcTemplate.queryForList("SELECT * FROM classroom");
System.out.println(list);
}
@Override
@TargetDataSource("ds_postgresql")
public void printUserList() {
List list = jdbcTemplate.queryForList("SELECT * FROM t_user");
System.out.println(list);
}
}
// DynamicDataSourceTest源码(启动配置类):
@Configuration
@ImportResource("classpath:/config/xspring-context-test.xml")
public class DynamicDataSourceTest {
private ApplicationContext applicationContext;
private DemoService demoService;
@Before
public void setUp() throws Exception {
applicationContext = XspringApplication.startup(DynamicDataSourceTest.class, null);
demoService = applicationContext.getBean("demoService", DemoService.class);
}
@Test
public void testDynamicDataSource() throws Exception {
demoService.printClassroomList();
demoService.printUserList();
}
}
自定义的XML配置文件xspring-context-test.xml内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<context:component-scan base-package="org.xspring" />
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>
待补充
参考:http://www.importnew.com/17673.html
https://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#resources
file:./config/ -> file:./ -> classpath:/config/ -> classpath:/
如果各个目录下都有相同的配置文件(如application.properties),则都会被加载进来(即不互斥),但如果多个jar包的相同路径下都存在一样的配置文件,则只会加载第一个匹配的文件(具体由classloader的加载顺序决定);如果每个文件中都包含相同的key,则最左边文件中的key具有最高的优先级,从源码注释也可证明这一点:
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
// ConfigFileApplicationListener中的描述:
// Note the order is from least to most specific (last one wins)
private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";
// ConfigFileApplicationListener.Loader类获取配置文件加载位置:
private Set<String> getSearchLocations() {
Set<String> locations = new LinkedHashSet<String>();
// User-configured settings take precedence, so we do them first
if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) {
for (String path : asResolvedSet(
this.environment.getProperty(CONFIG_LOCATION_PROPERTY), null)) {
if (!path.contains("$")) {
path = StringUtils.cleanPath(path);
if (!ResourceUtils.isUrl(path)) {
path = ResourceUtils.FILE_URL_PREFIX + path;
}
}
locations.add(path);
}
}
locations.addAll(
asResolvedSet(ConfigFileApplicationListener.this.searchLocations,
DEFAULT_SEARCH_LOCATIONS));
return locations;
}
]]>转载请注明出处:cloudnoter.com
建立用户与物品之间的联系:在用户没有明确目的的情况下帮助他们发现感兴趣的物品;为物品找到可能对它们感兴趣的用户。
个性化推荐的成功应用需要两个条件:
- 存在信息过载
- 用户大部分时候没有特别明确的需求
延伸阅读:
- Pandora研究人员关于音乐个性化推荐的演讲PPT
- 搜索雅虎公司发表的与个性化广告有关的论文
一个完整的推荐系统的参与者:用户、物品提供者、提供推荐系统的网站。在评测一个推荐算法时,需要同时考虑三方的利益,一个好的推荐系统是能够令三方共赢的系统。好的推荐系统不但能够准确预测用户的行为,而且能够扩展用户的视野,帮助用户发现那些他们可能会感兴趣,但却不那么容易发现的东西。同时,推荐系统不要能够帮助商家将那些被埋没在长尾中的好商品介绍给可能会对它们感兴趣的用户。
一般来说,一个新的推荐算法最终上线,需要完成上面所说的三个实验:
用户满意度
预测准确度:最重要的离线评测指标,根据离线推荐算法的不同研究方向,有相应更具体的预测准确度指标。
准确率(precision)/召回率(recall)度量。覆盖率:描述一个推荐系统对物品长尾的发掘能力。覆盖率有不同的定义,最简单的定义为推荐系统能够推荐出来的物品占总物品集合的比例。
$$Coverage=\frac{|\bigcup_{u \in U}R(u)|}{|I|}$$
上面的定义过于粗略,在信息论与经济学中有两个著名的指标可以用来定义覆盖率,第一个是信息熵:
$$H=-\sum_{i=1}^{n}p(i)\log_{2}p(i)$$这里的p(i)是物品i的流行度除以所有商品流行度之和。第二个指标是基尼系数(Gini Index):
$$G=\frac{1}{n - 1} \sum_{j=1}^{n}(2j - n - 1)p(i_j)$$这里$i_j$是按照物品流行度p()从小到大排序的物品列表中第j个物品。
多样性:
用户u的推荐列表的多样性的计算公式:
$$Diversity(R(u))=1 - \frac{\sum_{i,j \in R(u), i \neq j}s(i,j)}{\frac{1}{2}|R(u)|(|R(u)| - 1)}$$
推荐系统的整体多样性的计算公式:
$$Diversity=\frac{1}{|U|}\sum_{u \in U}Diversity(R(u))$$
新颖性
惊喜度
信任度
实时性
健壮性
商业目标
获取各种评测指标的途径如下表:
| - | 离线实验 | 问卷调查 | 在线实验 |
|---|---|---|---|
| 用户满意度 | N | Y | O |
| 预测准确度 | Y | Y | N |
| 覆盖率 | Y | Y | Y |
| 多样性 | O | Y | O |
| 新颖性 | O | Y | O |
| 惊喜度 | N | Y | N |
比如一个推荐算法虽然整体上性能不好,但可能在某种特定场景下性能比较好,而增加评测维度的目的就是为了知道一个算法在什么情况下性能最好。这样可以为融合不同推荐算法取得最好的整体性能带来参考。
在推荐系统评测报告中包含不同维度下的系统评测指标,就能帮助我们全面地了解推荐系统性能,力争在弱势算法中找优点,优势算法中找缺点。
本文档主要介绍ZooKeeper部署及日常管理
本章节包含与ZooKeeper部署有关的内容,具体来说包含下面三部分内容:
前两部分主要介绍如何在数据中心等生产环境上安装部署ZooKeeper,第三部分则介绍如何在非生产环境上(如为了评估、测试、开发等目的)安装部署ZooKeeper。
ZooKeeper框架由多个组件组成,有的组件支持全部平台,而还有一些组件只支持部分平台,详细支持情况如下:
Java客户端连接库,上层应用系统通过它连接至ZooKeeper集群。Java后台服务程序。C语言实现的客户端连接库,其与Java客户端库一样,上层应用(非Java实现)通过它连接至ZooKeeper集群。| 操作系统 | Client | Server | Native Client | Contrib |
|---|---|---|---|---|
| GNU/Linux | D + P | D + P | D + P | D + P |
| Solaris | D + P | D + P | / | / |
| FreeBSD | D + P | D + P | / | / |
| Windows | D + P | D + P | / | / |
| Mac OS X | D | D | / | / |
D:支持开发环境, P:支持生成环境, /:不支持任何环境
上表中未显式注明支持的组件在相应平台上可能不能正常运行。虽然ZooKeeper社区会尽量修复在未支持平台上发现的BUG,但并无法保证会修复全部BUG。
ZooKeeper需要运行在JDK6或以上版本中。若ZooKeeper以集群模式部署,则推荐的节点数至少为3,同时建议部署在独立的服务器上。在Yahoo!,ZooKeeper通常部署在运行RHEL系统的服务器上(服务器配置:双核CPU、2G内存、80G容量IDE硬盘)。
为了保证ZooKeeper服务的可靠性,您应该以集群模式部署ZooKeeper服务。只要半数以上的集群节点在线,服务将是可用的。因为ZooKeeper需要半数以上节点同意才能选举出Leader,所以建议ZooKeeper集群的节点数为奇数个。举个例子,对于有四个节点的集群只能应付一个节点宕机的异常,如果有两个节点宕机,则剩下两个节点未达到法定的半数以上选票,ZooKeeper服务将变为不可用。而如果集群有五个节点,则集群就可以应付二个节点宕机的异常。
提示:
正如《ZooKeeper快速入门》文档中所提到的,至少需要三个节点的ZooKeeper集群才具备容灾特性,因此我们强烈建议集群节点数为奇数。
通常情况下,生产环境下,集群节点数只需要三个。但如果为了在服务维护场景下也保证最大的可靠性,您也许会部署五个节点的集群。原因很简单,如果集群节点为三个,当你对其中一个节点进行维护操作,将很有可能因维护操作导致集群异常。而如果集群节点为5个,那你可以直接将维护节点下线,此时集群仍然可正常提供服务(就算四个节点中的任意一个突然宕机)。
您的冗余措施应该包括生产环境的各个方面。如果你部署三个节点的ZooKeeper集群,但你却将这三个节点都连接至同一个网络交换机,那么当交换机宕掉时,你的集群服务也一样是不可用的。
关于如何配置服务器使其成为集群中的一个成员节点的详细步骤如下(每个节点的操作类似):
1
2
3
4
5
6
7
8
tickTime=2000
dataDir=/var/lib/zookeeper/
clientPort=2181
initLimit=5
syncLimit=2
server.1=zoo1:2888:3888
server.2=zoo2:2888:3888
server.3=zoo3:2888:3888
您可以在配置参数章节中找到上面这些参数及其他参数的解释说明。这里简要说一下:集群中的每个节点都需要知道集群中其它节点成员的连接信息。通过上述配置文件中格式为
server.id=host:port:port的若干行配置信息您就可以实现这个目标。host与port意思很简单明了,不多作说明。而server.${id}代表节点ID,你需要为每个节点创建一个名为myid的文件,该文件存放于参数dataDir所指向的目录下。
myid文件的内容只有一行,其值为配置参数server.${id}中${id}的实际值。即服务器1对应的myid文件的内容为1,这个值在集群中必须保证其唯一性,同时必须处于[1, 255]之间。1
2
3
$ java -cp zookeeper.jar:lib/slf4j-api-1.6.1.jar:lib/
slf4j-log4j12-1.6.1.jar:lib/log4j-1.2.15.jar:conf \
org.apache.zookeeper.server.quorum.QuorumPeerMain zoo.cfg
QuorumPeerMain启动一个ZooKeeper服务,JMX管理bean也同时被注册,通过这些JMX管理bean,你就可以在JMX管理控制台上对ZooKeeper服务进行监控管理。ZooKeeper的JMX文档有更详细的介绍信息。同时,你也可以在
$ZOOKEEPER_HOME/bin目录下找到ZooKeeper的启动脚本zkServer.sh。
Java环境下,你可以运行下面的命令连接至已启动的ZooKeeper服务节点并执行一些简单的操作1
$ bin/zkCli.sh -server 127.0.0.1:2181
如果你想部署ZooKeeper以便于开发测试,那你可以使用单机部署模式。然后安装Java或C客户端连接库,同时在配置文件中将服务器信息与开发机绑定。
具体安装部署步骤也集群部署模式类似,唯一不同的是zoo.cfg配置文件更简单一些。你可以从《ZooKeeper快速入门》文档中的相关章节获取详细的操作步骤。
关于安装客户端连接库的相关信息,你可以从ZooKeeper Programmer’s Guide文件的Bindings章节中获取。
这部分包含ZooKeeper运行与维护相关的信息,其包含如下几个主题:
ZooKeeper可靠性依赖于两个基本的假设:
为了最大可能的保证上述两个前提假设能够成立,这里有几个点需要考虑。一些是关于跨服务器(节点之间)需求的,而另一些是关于集群中每个节点服务器的考虑。
为了启用ZooKeeper服务,集群中半数以上的节点需要能够相互通信。为了创建一个可以支撑F个节点异常的集群,你需要部署2xF+1个节点。即一个包含三个节点的集群可以应对一个节点异常的情况,一个包含五个节点的集群则可以应对二个节点异常的情况,依此类推。需要注意的是,一个包含六个节点的集群也只能应对二个节点失败的情况,因为三个节点并未超过半数。因此,ZooKeeper集群中的节点数通常为奇数。
为了实现最大限度的容灾能力,你应该尽力使集群节点发生异常时不影响其它节点(即保持节点部署上尽可能的独立)。举个例子,当大部分的机器共享同一个交换机,若这个交换机挂了将使关联的服务都下线。共享电源电路、冷却系统等也是同样的道理(有同样的单点问题)。
如果ZooKeeper服务需要与其它应用竞争存储、CPU、网络、内存等资源时,其性能将显著下降。机器必须为ZooKeeper服务提供持久化保障,因为ZooKeeper需要先使用存储设备来保存变更日志,然后才允许变更操作提交。如果你想确保ZooKeeper操作不会因存储而挂起,那你应该意识到这个依赖关系并重视起来。这里有几个事情可以最小化这类问题所带来的消极影响。
无
无
无
ZooKeeper的长期维护需求几乎没有,然而你仍然需要注意如下几件事情:
ZooKeeper的数据目录包含集群上特殊存储结点znode数据的持久化拷贝文件。文件中包含快照与事务日志文件。znode上数据的变更将同时会被追加至事务日志中。当这些日志不断增长时,所有znode的当前状态的快照文件将被写回文件系统。这个快照将取代之前的日志。
ZooKeeper服务器默认情况下不会移除旧的快照与日志文件,删除快照与日志这项工作是管理员的责任。因为每个服务环境是不一样的,所以管理这些快照与文件的需求也是不同一。PurgeTxnLog工具实现了一个简单的保留策略,管理员可以使用这个工具,API docs 中包含有详细的使用说明。
下面的例子的作用为:保留最近<count>个快照及相关的日志,其它的快照及相关日志则删除。 <count>的值通常要大于3(虽然不是必须的,但提供3个备份时将极大降低日志损坏的可能性)。您可以在ZooKeeper服务器上运行cron脚本来每天清除过期日志。
1
2
$ java -cp zookeeper.jar:lib/slf4j-api-1.6.1.jar:lib/slf4j-log4j12-1.6.1.jar:lib/
log4j-1.2.15.jar:conf org.apache.zookeeper.server.PurgeTxnLog <dataDir> <snapDir> -n <count>
在V3.4.0版本中引入的自动清除快照及相关事务日志的特性可以通过配置参数autopurge.snapRetainCount 与 autopurge.purgeInterval来启用。更多的使用见后续的配置参数(高级配置)章节。
通过本文档的日志章节可知,ZooKeeper将使用Log4j内置的日志滚动覆盖功能最多生成N个日志文件。Log4j的配置示例可在发行版本tar包的conf/log4j.properties文件中找到。
你需要一个监督进程来管理ZooKeeper服务进程(本质上为一个Java进程)。ZooKeeper服务具有"快速失败"的特性,即只要服务发生不可恢复的错误,则ZK进程直接退出。因为ZK集群是高可用的,所有集群中的一个ZK服务虽然异常退出了,但整个集群仍然是可用的,仍然可对外提供服务。同时由于集群可以自行恢复特性,一旦此前异常退出的服务器重启后,它将自动重新加入集群中而不需要任何的人工干预。
因此,我们就需要一个类似daemontools或SMF的工具来管理ZK服务,确保进程异常退出后可以自动被重启并快速重新加入集群。
ZK服务可以使用如下两种方式进行监控:
ZK使用V1.2版本的Log4j作为其日志组件。ZK默认的日志配置文件位于发行版本的conf目录下。Log4j需要log4j.properties文件,这个文件要么位于ZK运行时所在的目录,要么位于classpath目录。
更多的信息见 Log4j Default Initialization Procedure 。
文件损坏导致服务器无法启动问题
ZK服务器可能会因无法正常读取数据导致启动失败。出现这个问题可能是因为ZK服务器上的事务日志文件损坏了。你可以从ZK的log4j日志中看到一些与ZK数据加载相关的IOException异常。在这种情况下,首先确保集群中的其他服务器能正常工作。可以使用stat命令查看它们是否处于健康状态。当你已经确认集群中其它服务器都在运行中,你可以进一步清理异常中断的服务器上的数据库:删除$data_dir/version-2目录与$data_log_dir/version-2目录下的内容,然后重启服务即可。
ZooKeeper的行为是由其配置文件$ZK_HOME\conf\zoo.cfg来控制的。由于这个配置文件是设计好的,因此当ZK集群中每个成员的部署结构(磁盘路径等)都一样时,这个配置文件可以被所有成员共用。如果成员服务器使用不同的配置文件,一定要注意保证各个服务器的配置文件的服务器列表信息是匹配的。
下面是部署一个ZK集群时,配置文件必须要设置的参数信息(最简配置):
clientPort
客户端连接监听端口,即客户端发起连接请求时,都会尝试连接至ZK服务器的该端口。
dataDir
ZK内存数据库快照的保存位置,除非另有规定,否则事务日志也会保存至该位置。
一定要注意事务日志的存储位置。一个专用的事务日志设备是保证高性能的关键。如果将事务日志存放于IO比较频繁的设备将产生不利的性能效果。
tickTime
计时时间片长度(单位:毫秒),它是ZK中所有与时间有关参数的基本时间单元(即当某参数的值为X,则其语义可这样理解:某参数的值为tickTime的X倍)。它通常用于日常的心跳发送周期、超时时间等。
比如,当tickTime=2000,minSessionTimeout=2,
则会话超时的最小值为:tickTime*minSessionTimeout=4000ms。
本节所介绍的配置参数是可选的。你可以使用它们进一步调整ZK集群的行为。有些参数也可以通过形如zookeeper.keyword之类的Java系统属性的方式来配置。下面的参数详细介绍中有标明哪些参数可以通过Java系统属性方式配置。
dataLogDir
这个选项会让机器将事务日志直接写到dataLogDir所指定的位置,而不再是默认的dataDir。这将允许使用专用的日志设备,同时可有效避免快照操作与日志操作所带来的IO竞争。
使用专用的日志设备将极大的影响系统吞吐量与系统时延。强烈推荐使用专用的日志设备,并将dataLogDir指向该专用设备的一个目录,然后也要确保dataDir参数所指向的目录未指向专用的日志设备。
globalOutstandingLimit
(Java系统属性: zookeeper.globalOutstandingLimit)
客户端提交的请求数通过会比ZK的处理速度快,特别是存在大量客户端连接的情况。为了防止因队列中的请求数多过导致ZK运行过程中出现内存溢出问题,ZK将控制客户端的请求数,这样将可以保证系统请求数不会多于globalOutstandingLimit参数所指定的值,系统默认值为1000。
preAllocSize
(Java系统属性: zookeeper.preAllocSize)
To avoid seeks ZooKeeper allocates space in the transaction log file in blocks of preAllocSize kilobytes. The default block size is 64M. One reason for changing the size of the blocks is to reduce the block size if snapshots are taken more often. (Also, see snapCount).
snapCount
(Java系统属性: zookeeper.snapCount)
ZK将事务写入事务日志文件中。当snapCount个事务被写入日志文件后,ZK将创建一个快照,然后一个新的事务日志文件会被创建。该参数的默认值为:100000。
maxClientCnxns
限制同一个客户端连接(通过IP标识唯一性)至一个ZK集群节点的并发连接数(Socket级别)。这个参数主要是用来防止某些类型的DoS攻击,包括file-descriptor exhaustion。该参数默认值为60,如果设置为0则意味着关闭并发控制限制功能。
clientPortAddress
V3.3.0引入: 客户端连接监听地址(IPv4,IPv6,主机名), 这个参数是可选的。默认情况下,所有连接至clientPort参数指定端口号的连接都会被接受,不管这些连接是来自于哪个网络接口。
minSessionTimeout
V3.3.0引入: 最小会话超时时间(毫秒),在会话期间服务器将允许来自客户端的协商。该参数默认值为2倍的tickTime。
maxSessionTimeout
V3.3.0引入: 最小会话超时时间(毫秒),在会话期间服务器将允许来自客户端的协商。该参数默认值为20倍的tickTime。
fsync.warningthresholdms
(Java系统属性: zookeeper.fsync.warningthresholdms)
V3.3.4引入: 当事务日志(WAL)中的fsync操作时间超过该参数所设置的值时,一个告警信息将被输出到log4j日志中。该参数默认值为1000毫秒,该值只能通过系统属性的方式设置。
autopurge.snapRetainCount
V3.4.0引入: 当启用该参数时,ZK将仅保留dataDir与dataLogDir目录下最近的快照及事务日志数据,保留的数量由该参数设置值决定;然后清除其它过期数据。默认值为3,最小值为3。
autopurge.purgeInterval
V3.4.0引入: 以小时为单位的时间间隔,即每隔N小时将触发一次快照及事务日志清理任务。设置一个正数(大于等于1)即可启用自动清理功能,默认值为0。
syncEnabled
(Java系统属性: zookeeper.observer.syncEnabled)
V3.4.6, V3.5.0引入: 集群中角色为observer的成员实时的将事务日志与快照数据写回磁盘,就好像它们是业务的参与者一样。这将减少这些成员的重启恢复时间。如果将这个参数设置为false,将禁用这个特性。默认为true。
与集群有关的参数选项
本节所介绍的参数选项主要用于服务器集群中,也就是说当部署一个集群时可以使用这些参数。
electionAlg
配置集群使用的选举算法。0值代表基于UDP的版本,1值代表基于UDP的无认证的快速Leader选举版本,2值代表基于UDP的有认证的快速Leader选举版本,3值代表基于TCP快速Leader选举版本。算法3为默认值。
0,1,2三个leader选举的实现方案现已被废弃。我们计划在下一个版本中移除,也就是说后续只有
FastLeaderElection可用。
initLimit
允许集群中follower节点连接及同步数据至leader节点的时间耗时(以tickTime为单位)。如果由ZK管理的数据量比较大,则可以按需增加这个值。
leaderServes
(Java系统属性: zookeeper.leaderServes)
配置集群中的leader节点是否接受客户端连接,默认为yes。Leader节点主要负责协调集群节点之间数据的更新。对于与协调更新有关的吞吐量远高于客户端的读请求的吞吐量,则leader节点可设置为不接受客户端的连接请求,而专注于协调集群节点间的数据更新。默认值为yes意味着leader节点默认情况下将会接受客户端的连接请求。
当集群超过3个以上的节点时,推荐打开这个开关。
server.x=[hostname]:nnnnn[:nnnnn]
ZK集群由多个服务器节点组成。当服务器节点启动时,ZK到$data_dir目录下找到myid文件,并根据里面内容确定其身份编号及连接端口信息。myid文件中包含当前服务器节点的编号(以ASCII格式呈现),这个编号与上述配置参数server.x中的x匹配的则为当前服务器节点的IP与监听端口。
配置参数中的服务器列表信息必须与ZK集群服务器节点的一一匹配。
配置参数中有两个端口号,第一个端口用于follower节点与leader节点间的通信,第二个端口用于集群leader选举。只有electionAlg参数值为非0值时,选举用的端口号(第二个)才是必须的。若electionAlg参数值为0值时,则第二个端口号是非必要的。如果你想在单机上测试多个服务器,则可以使用不同的端口号。
syncLimit
允许集群follower节点与leader节点进行数据同步的总耗时(以tickTime为单位)。如果follower的数据与leader的数据由于同步不及时导致差异太大,则这些follower节点将被移出集群。
group.x=nnnnn[:nnnnn]
Enables a hierarchical quorum construction."x"是组编号,而"="右边则是用":"分隔的服务器编号。每组的服务器成员编号不能有交集,同时所有组的节点成员的并集刚好是集群的所有成员。
你可在这里找到使用的例子。
weight.x=nnnnn
与"group"参数搭配使用。它为集群中的每个成员提供一个权值,这个权值在进行进行法定人数选举投票时会用到。There are a few parts of ZooKeeper that require voting such as leader election and the atomic broadcast protocol. By default the weight of server is 1. If the configuration defines groups, but not weights, then a value of 1 will be assigned to all servers.
你可在这里找到使用的例子。
cnxTimeout
(Java系统属性: zookeeper.cnxTimeout)
为leader选举通知消息而打开的连接设置一个超时时间。只有electionAlg值为3时这个参数才有用。默认值为5秒。
4lw.commands.whitelist
(Java系统属性: zookeeper.4lw.commands.whitelist)
V3.4.10引入: 这个属性包含了一系统用逗号分隔的由4个字符组成的命令列表。它之所以被引入是为了提供更灵活的粒度控制机制来决定哪些ZK命令可以执行。因此借助这个参数,用于可以根据需要来关闭某些命令。默认情况下,若未指定该参数,则除了wchp与wchc以外的命令都可以执行。如果配置了该参数,则只有参数中列出的命令可以执行(被连接的客户端调用执行)。
下面是一个只启用了stat,ruok,conf,isro命令的配置:4lw.commands.whitelist=stat, ruok, conf, isro
用户也可以使用通配符来进行配置,如:4lw.commands.whitelist=*
认证与授权参数选项
本节介绍的内容主要是用于配置认证/授权相关功能的。
zookeeper.DigestAuthenticationProvider.superDigest
(Java系统属性: zookeeper.DigestAuthenticationProvider.superDigest),只能通过Java系统属性方式配置。
该特性默认是禁用的
V3.2版引入: Enables a ZooKeeper ensemble administrator to access the znode hierarchy as a "super" user. In particular no ACL checking occurs for a user authenticated as super.
org.apache.zookeeper.server.auth.DigestAuthenticationProvider can be used to generate the superDigest, call it with one parameter of super:<password>. Provide the generated super:<data> as the system property value when starting each server of the ensemble.
When authenticating to a ZooKeeper server (from a ZooKeeper client) pass a scheme of "digest" and authdata of super:<password>. Note that digest auth passes the authdata in plaintext to the server, it would be prudent to use this authentication method only on localhost (not over the network) or over an encrypted connection.
ZooKeeper可以响应一个小规模的命令集。命令集中的每个命令由4个字母组成。
你可以通过nc或telnet向ZooKeeper服务器的客户端监听端口发送命令。其中有三个命令是比较令人感兴趣的:stat会输出与服务器、已连接的客户端相关的基本信息;srvr与cons会输出服务器与各个连接的进一步的详细信息。
conf
V3.3.0版引入: 打印ZooKeeper配置的详细信息。
cons
V3.3.0版引入: 列出所有连接至本服务器的客户端连接/会话的详细信息。包括收发数据包数量、会话ID、操作时延(响应时间)、上一次执行的操作等信息。
crst
V3.3.0版引入: 重置所有连接/会话的统计数据。
dump
Lists the outstanding sessions and ephemeral nodes. This only works on the leader.
envi
打印ZK服务环境信息。
ruok
测试ZK服务器是否处于正常运行状态。若服务器正常运行,则会返回imok响应;反之则压根不会返回任何响应信息。imok响应并不意味着该服务器已经加入ZK法定成员中,而仅代表服务器处于活动状态且与某个特定的客户端口建立了绑定关系。你可以使用stat命令查看该服务器是否加入法定投票成员及客户端连接等详细状态信息。
srst
重置服务器统计数据。
srvr
V3.3.0版引入: 列出服务器的完整的详细信息。
stat
列出服务器及已连接至该服务器的客户端的简要信息。
wchs
V3.3.0版引入: 列出watch该服务器的客户端的简要信息。
wchc
V3.3.0版引入: 从session视角列出watch该服务器的客户端的详细信息。该命令将输出session(connection)所watch的路径的相关信息。有一点要注意下,执行这个命令的性能损耗与watch的数量有关,请谨慎使用。
wchp
V3.3.0版引入: 从path视角列出watch该path的相关客户端session。有一点要注意下,执行这个命令的性能损耗与watch的数量有关,请谨慎使用。
mntr
V3.4.0版引入: 输出可用于监控集群健康状态的参数列表。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ echo mntr | nc localhost 2185
zk_version 3.4.0
zk_avg_latency 0
zk_max_latency 0
zk_min_latency 0
zk_packets_received 70
zk_packets_sent 69
zk_outstanding_requests 0
zk_server_state leader
zk_znode_count 4
zk_watch_count 0
zk_ephemerals_count 0
zk_approximate_data_size 27
zk_followers 4 - only exposed by the Leader
zk_synced_followers 4 - only exposed by the Leader
zk_pending_syncs 0 - only exposed by the Leader
zk_open_file_descriptor_count 23 - only available on Unix platforms
zk_max_file_descriptor_count 1024 - only available on Unix platforms
The output is compatible with java properties format and the content may change over time (new keys added). Your scripts should expect changes.
ATTENTION: Some of the keys are platform specific and some of the keys are only
exported by the Leader. The output contains multiple lines with the following format:key \t value
Here’s an example of the ruok command:
1
2
$ echo ruok | nc 127.0.0.1 5111
imok
ZK将其业务数据保存在数据目录,将其事务日志保存在事务日志目录。默认情况下数据目录与事务日志目录是相同的。服务器可以(应该)将事务日志目录与数据目录分开存储。如果将事务日志存储在专用的日志设备上,则吞吐量上升的同时,时延还会下降。
这个目录下包含两个文件:
snapshot.<zxid> - 这个文件保存有数据树结构的模糊快照。每个ZooKeeper服务器都有一个唯一标识ID,这个ID用于两个地方:myid文件与配置文件。位于数据目录下的myid文件用于标识ZK服务器。配置文件zoo.cfg中列出了集群节点间相互通信的配置信息(IP或主机名、监听端口等),每行配置信息通过Server ID来标识。当ZooKeeper服务实例启动时,其会从myid文件中获取它的标识ID,然后再从配置文件zoo.cfg中读取匹配该ID的服务器配置信息,根据配置信息启动相关端口的监听服务。
The snapshot files stored in the data directory are fuzzy snapshots in the sense that during the time the ZooKeeper server is taking the snapshot, updates are occurring to the data tree. The suffix of the snapshot file names is the zxid, the ZooKeeper transaction id, of the last committed transaction at the start of the snapshot. Thus, the snapshot includes a subset of the updates to the data tree that occurred while the snapshot was in process. The snapshot, then, may not correspond to any data tree that actually existed, and for this reason we refer to it as a fuzzy snapshot. Still, ZooKeeper can recover using this snapshot because it takes advantage of the idempotent nature of its updates. By replaying the transaction log against fuzzy snapshots ZooKeeper gets the state of the system at the end of the log.
日志目录包含ZK的事务日志。当任何更新生效前,ZK会确保代表该更新的事务会被写入稳定的存储中。每创建一次快照,会创建一个新的事务日志文件,该日志文件名的后缀为第一个写入该文件的事务日志的zxid。
单机模式与集群模式下的快照与日志文件的格式是一样的,因此你可以从集群复制模式环境中获取相关快照及日志文件到单机开发环境中进行问题排查。
通过使用旧的日志及快照文件,你可以分析ZK服务器以前的状态甚至可以恢复至该旧状态。LogFormatter类允许管理员分析一个日志中的相关事务。
ZK负责快照及日志的创建,但其不会删除这些文件。数据及日志文件的保留策略由外部应用实现。服务器自身只需要最近一次的模糊快照以及从开始创建该快照以来的事务日志文件即可。可以查看"维护"章节来查看更详细的与保留策略相关的配置信息。
存储在这些文件中的数据是未加密的。在存储敏感数据的场景下,有必要采取一定的措施来阻止未授权访问请求。这些预防措施都不属于ZK的功能范围,因此相关配置信息也是独立的,本文档不多做介绍。
下面是几个在配置ZK服务时经常碰到的问题及解决办法:
服务器列表数据不一致
The list of ZooKeeper servers used by the clients must match the list of ZooKeeper servers that each ZooKeeper server has. Things work okay if the client list is a subset of the real list, but things will really act strange if clients have a list of ZooKeeper servers that are in different ZooKeeper clusters. Also, the server lists in each Zookeeper server configuration file should be consistent with one another.
incorrect placement of transasction log
The most performance critical part of ZooKeeper is the transaction log. ZooKeeper syncs transactions to media before it returns a response. A dedicated transaction log device is key to consistent good performance. Putting the log on a busy device will adversely effect performance. If you only have one storage device, put trace files on NFS and increase the snapshotCount; it doesn’t eliminate the problem, but it should mitigate it.
Java堆大小配置不正确
You should take special care to set your Java max heap size correctly. In particular, you should not create a situation in which ZooKeeper swaps to disk. The disk is death to ZooKeeper. Everything is ordered, so if processing one request swaps the disk, all other queued requests will probably do the same. the disk. DON’T SWAP.
Be conservative in your estimates: if you have 4G of RAM, do not set the Java max heap size to 6G or even 4G. For example, it is more likely you would use a 3G heap for a 4G machine, as the operating system and the cache also need memory. The best and only recommend practice for estimating the heap size your system needs is to run load tests, and then make sure you are well below the usage limit that would cause the system to swap.
暴露在公网
一个ZK集群应该部署在一个可靠的计算环境中,因此推荐部署在防火墙后面。
For best results, take note of the following list of good Zookeeper practices: For multi-tennant installations see the section detailing ZooKeeper "chroot" support, this canbe very useful when deploying many applications/services interfacing to a single ZooKeeper cluster.
]]>自从平台升级到3.0后,应用的JVM变得非常不稳定,主要体现为以下三个问题:
问题1相对好解决,先用jmap将堆快照dump出来,用mat分析了下,根据GC-ROOT找到引用路径即可,泄漏原因为:平台自研JPA组件的SQLQuery在实现lazy load时,由于CGLib使用不当(在向当前线程注册回调方法拦截器时,在使用完之后未及时注销)导致的查询结果缓存被线程池中的线程引用,在线程池容量开得比较大时最终将导致OOM异常。
以前从没碰到这种情况,方法区的内存大小在应用启动后应该是处于一个相对稳定的状态(因为大部分类在启动时就已经加载完了,就算使用CGLib动态生成代理类也应该是有一个上限,最多就是全部类的一倍),但问题2明显不属于这种情况,不管开多大的内存给方法区(通过-X:MaxPermSize=xxxM设置大小),应用总能在几分钟内持续升到最高值并触发FullGC,GC结束后,方法区占用内存降至接近0M(此处就发生的class unload),然后又进入新一轮的飙升周期(此处就发生class loader)。
刚开始以为仍然是JPA组件使用CGLib不当的问题,认为是为了实现lazy load及权限控制时使用了过多的动态代理(每个Action,Model,Service都被创建为动态代理,更不合理的时每个model的get方法都使用ProxyMethodInterceptor,问当事人原因,其答复说为了lazy load,但其实只有关联字段,集合字段才有必要lazy load)。基于此做了修改,但测试结果还是没解决问题:因为JPA中并不是每次都创建一个新的proxy,而是根据class做了缓存的,因此只能另找办法。
既然是方法区的问题,那是否可以将方法区的内容dump出来呢?于是查看了下jmap参数,其中的有-permstat可以用:jmap -permstat <pid>
结果如下(截取)
1
2
3
4
5
6
7
8
9
10
11
12
13
25007 intern Strings occupying 2799672 bytes.
class_loader classesbytes parent_loaderalive? type
<bootstrap> 262415108248 null live <internal>
0x000000076f045910 38758792 0x00000007617d4f70dead com/atomikos/util/ClassLoadingHelper$1@0x0000000741cb86e8
0x000000076f942f10 38758792 0x00000007617d4f70dead com/atomikos/util/ClassLoadingHelper$1@0x0000000741cb86e8
0x00000007d00e9ba0 40816816 0x00000007617d4f70dead com/atomikos/util/ClassLoadingHelper$1@0x0000000741cb86e8
0x00000007dd170c08 40816816 0x00000007617d4f70dead com/atomikos/util/ClassLoadingHelper$1@0x0000000741cb86e8
0x000000079e2f0070 40816816 0x00000007617d4f70dead com/atomikos/util/ClassLoadingHelper$1@0x0000000741cb86e8
0x00000007cd6b1140 13112 null dead sun/reflect/DelegatingClassLoader@0x0000000740067648
0x000000076fb5d130 38758792 0x00000007617d4f70dead com/atomikos/util/ClassLoadingHelper$1@0x0000000741cb86e8
0x000000076fbe47c0 38758792 0x00000007617d4f70dead com/atomikos/util/ClassLoadingHelper$1@0x0000000741cb86e8
com/atomikos/util/ClassLoadingHelper1ドル:是一个匿名内部类(该类是一个加载器),通过这个内部类加载器作为JDK Proxy.newProxyInstance()方法的参数,而后者就会生产大量的以Proxy$为前缀的动态类,并且未做任何缓存。
atomikos大家应该都很清楚:JTA的一个实现,但是哪个组件调用了这个工具类呢?通过断点分析,原来是JPA又自己写了个什么数据库连接池,池中的每个连接都是ProxyConnection,而池又好像失效的,频繁的回收,创建...
到目前为止,我一直没搞清楚为何要用代理类型的连接,这代理的作用从代码中也没看出个门道来,也不是为做监控。
原因定位到了,解决办法就很简单了:直接用阿里的druid替换掉。测试结果证明前面的分析是正确的。
]]>转载请注明出处:cloudnoter.com
Pacemaker的管理工具主要有两种:crmsh、pcs(Pacemaker/Corosync configuration system),本文将同时介绍这两种命令行工具。
从CentOS6.4以后开始采用PCS替代crmsh来管理pacemaker集群(PCS专用于pacemaker+corosync的设置工具,其有CLI和web-based GUI界面)
文档来源于Pacemaker的Github官网
以XML格式显示
1
2
3
4
# crmsh
crm configure show xml
# pcs
pcs cluster cib
以非XML格式显示[To show a simplified (non-xml) syntax]
1
2
3
4
# crmsh
crm configure show
# pcs
pcs config
1
2
3
4
# crmsh
crm status
#pcs
pcs status
也可以这样:
1
crm_mon -1
使节点进入Standby状态(Put node in standby)
1
2
3
4
# crmsh
crm node standby pcmk-1
# pcs
pcs cluster standby pcmk-1
使节点从Standby状态恢复(Remove node from standby)
1
2
3
4
# crmsh
crm node online pcmk-1
# pcs
pcs cluster unstandby pcmk-1
crm has the ability to set the status on reboot or forever.
pcs can apply the change to all the nodes.
1
2
3
4
# crmsh
crm configure propertystonith-enabled=false
# pcs
pcs propertyset stonith-enabled=false
classes1
2
3
4
# crmsh
crm ra classes
# pcs
pcs resource standards
1
2
3
4
5
6
7
8
9
10
11
12
# crmsh
crm ra list ocf
crm ra list lsb
crm ra list service
crm ra list stonith
# pcs
pcs resource agents ocf
pcs resource agents lsb
pcs resource agents service
pcs resource agents stonith
pcs resource agents
您也可以通过provider进一步过滤:
1
2
3
4
# crmsh
crm ra list ocf pacemaker
# pcs
pcs resource agents ocf:pacemaker
1
2
3
4
# crmsh
crm ra meta IPaddr2
# pcs
pcs resource describe IPaddr2
Use any RA name (like IPaddr2) from the list displayed with the previous command
You can also use the full class:provider:RA format if multiple RAs with the same name are available :
1
2
3
4
# crmsh
crm ra meta ocf:heartbeat:IPaddr2
# pcs
pcs resource describe ocf:heartbeat:IPaddr2
1
2
3
4
5
6
# crmsh
crm configure primitive ClusterIP ocf:heartbeat:IPaddr2 \
params ip=192.168.122.120 cidr_netmask=32 \
op monitor interval=30s
# pcs
pcs resource create ClusterIP IPaddr2 ip=192.168.0.120 cidr_netmask=32
The standard and provider (ocf:heartbeat) are determined automatically since IPaddr2 is unique.
The monitor operation is automatically created based on the agent’s metadata.
1
2
3
4
# crmsh
crm configure show
# pcs
pcs resource show
crmsh also displays fencing resources.
The result can be filtered by supplying a resource name (IE ClusterIP):
1
2
3
4
# crmsh
crm configure show ClusterIP
# pcs
pcs resource show ClusterIP
crmsh also displays fencing resources.
1
2
3
4
# crmsh
crm resource show
# pcs
pcs stonith show
pcs treats STONITH devices separately.
1
2
3
4
# crmsh
crm ra meta stonith:fence_ipmilan
# pcs
pcs stonith describe fence_ipmilan
1
2
3
4
# crmsh
crm resource start ClusterIP
# pcs
pcs resource enable ClusterIP
1
2
3
4
# crmsh
crm resource stop ClusterIP
# pcs
pcs resource disable ClusterIP
1
2
3
4
# crmsh
crm configure delete ClusterIP
# pcs
pcs resource delete ClusterIP
1
2
3
4
# crmsh
crm resource param ClusterIP set clusterip_hash=sourceip
# pcs
pcs resource update ClusterIP clusterip_hash=sourceip
crmsh also has an edit command which edits the simplified CIB syntax
(same commands as the command line) via a configurable text editor.
1
2
# crmsh
crm configure edit ClusterIP
Using the interactive shell mode of crmsh, multiple changes can be
edited and verified before committing to the live configuration.
1
2
3
4
5
# crmsh
node-01$ crm configure # 进入crmsh上下文模式
crm(live)configure$ edit
crm(live)configure$ verify
crm(live)configure$ commit
1
2
3
4
# crmsh
crm resource param ClusterIP delete nic
# pcs
pcs resource update ClusterIP ip=192.168.0.98 nic=
1
2
3
4
# crmsh
crm configure show type:rsc_defaults
# pcs
pcs resource defaults
1
2
3
4
# crmsh
crm configure rsc_defaults resource-stickiness=100
# pcs
pcs resource defaults resource-stickiness=100
1
2
3
4
# crmsh
crm configure show type:op_defaults
# pcs
pcs resource op defaults
1
2
3
4
# crmsh
crm configure op_defaults timeout=240s
# pcs
pcs resource op defaults timeout=240s
1
2
3
4
# crmsh
crm configure colocation website-with-ip INFINITY: WebSite ClusterIP
# pcs
pcs constraint colocation add ClusterIP with WebSite INFINITY
With roles
1
2
3
4
# crmsh
crm configure colocation another-ip-with-website inf: AnotherIP WebSite:Master
# pcs
pcs constraint colocation add Started AnotherIP with Master WebSite INFINITY
1
2
3
4
# crmsh
crm configure order apache-after-ip mandatory: ClusterIP WebSite
# pcs
pcs constraint order ClusterIP then WebSite
With roles:
1
2
3
4
# crmsh
crm configure order ip-after-website Mandatory: WebSite:Master AnotherIP
# pcs
pcs constraint order promote WebSite then start AnotherIP
1
2
3
4
# crmsh
crm configure location prefer-pcmk-1 WebSite 50: pcmk-1
# pcs
pcs constraint location WebSite prefers pcmk-1=50
With roles:
1
2
3
4
# crmsh
crm configure location prefer-pcmk-1 WebSite rule role=Master 50: \#uname eq pcmk-1
# pcs
pcs constraint location WebSite rule role=master 50 \#uname eq pcmk-1
1
2
3
4
5
crm resource move WebSite pcmk-1
pcs resource move WebSite pcmk-1
crm resource unmove WebSite
pcs resource clear WebSite
A resource can also be moved away from a given node:
1
2
crm resource ban Website pcmk-2
pcs resource ban Website pcmk-2
Remember that moving a resource sets a stickyness to -INF to a given node until unmoved
1
2
crm resource trace Website
# pcs不支持
1
2
crm resource cleanup Website
pcs resource cleanup Website
1
2
3
4
crm resource failcount Website show pcmk-1
crm resource failcount Website set pcmk-1 100
# pcs不支持
pcs deals with constraints differently. These can be manipulated by the command above as well as the following and others
1
2
3
# 下面这行命令的list可以省略,使用full选项是为了显示相关的id
pcs constraint list --full
pcs constraint remove cli-ban-Website-on-pcmk-1
使用crmsh命令删除约束的方式与删除资源的命令一样
Removing a constraint in crmsh uses the same command as removing a resource.
1
crm configure remove cli-ban-Website-on-pcmk-1
The show and edit commands in crmsh can be used to manage
resources and constraints by type:
1
2
crm configure show type:primitive
crm configure edit type:colocation
1
2
crm configure clone WebIP ClusterIP meta globally-unique=true clone-max=2 clone-node-max=2
pcs resource clone ClusterIP globally-unique=true clone-max=2 clone-node-max=2
1
2
3
4
5
6
crm configure ms WebDataClone WebData \
meta master-max=1 master-node-max=1 \
clone-max=2 clone-node-max=1 notify=true
pcs resource master WebDataClone WebData \
master-max=1 master-node-max=1 \
clone-max=2 clone-node-max=1 notify=true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# crmsh通过crm命令进入crmsh上下文模式,直接对CIB文档结构进行操作,最后再一次性commit
crmsh # crm
crmsh # cib new drbd_cfg
crmsh # configure primitive WebData ocf:linbit:drbd params drbd_resource=wwwdata \
op monitor interval=60s
crmsh # configure ms WebDataClone WebData meta master-max=1 master-node-max=1 \
clone-max=2 clone-node-max=1 notify=true
crmsh # cib commit drbd_cfg
crmsh # quit
# pcs则先基于本地文件方式批量设置CIB参数,然后再通过push操作使配置生效
pcs # pcs cluster cib drbd_cfg
pcs # pcs -f drbd_cfg resource create WebData ocf:linbit:drbd drbd_resource=wwwdata \
op monitor interval=60s
pcs # pcs -f drbd_cfg resource master WebDataClone WebData master-max=1 master-node-max=1 \
clone-max=2 clone-node-max=1 notify=true
pcs # pcs cluster push cib drbd_cfg
Create a resource template based on a list of primitives of the same
type
1
crm configure assist template ClusterIP AdminIP
Display information about recent cluster events
1
2
3
4
crmsh # crm history
crmsh # peinputs
crmsh # transition pe-input-10
crmsh # transition log pe-input-10
Create and apply multiple-step cluster configurations including
configuration of cluster resources
1
2
3
4
5
6
7
crmsh # crm script show apache
crmsh # crm script run apache \
id=WebSite \
install=true \
virtual-ip:ip=192.168.0.15 \
database:id=WebData \
database:install=true
]]>转载请注明出处:cloudnoter.com
中文乱码问题在Java Web开发中经常碰到,大部分原因是后端与前端的编码不一致造成的(如tomcat的默认编码为ISO-8859-1,而前端为GBK),解决办法也简单,只需要加一个CharsetEncodingFilter就行。但本文要讲的不是这一类总是,而是纯粹的后端问题。
假设MySQL的默认CharSet为UTF-8,应用及部署环境也为UTF-8
1
2
3
4
5
6
7
8
CREATE TABLE "ipms_device_feature" (
"ID" int(11) NOT NULL AUTO_INCREMENT,
"DEVICE_SERIAL_NUMBER" varchar(100) NOT NULL DEFAULT '' COMMENT '设备SN',
"DEVICE_IP" varchar(32) NOT NULL DEFAULT '' COMMENT '设备IP',
"FEATURES" json DEFAULT NULL COMMENT '设备巡检指标集',
"UPDATETIME" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY ("ID")
)
1
show full fields from ipms_device_feature
结果如下:
mysql_json
上图中
features这个字段的Collation列为Null
1
2
# 使用如下SQL时,JDBC在解析返回的数据(包含中文)时会出现乱码
select features from ipms_device_feature
第一步:使用MySQL提供的json_unquote方法
1
select json_unquote(features) as features from ipms_device_feature
第二步:在Java中调用上面的SQL时,将会返回一个byte数组,因此只需要通过String提供的方法进行转码就行。
1
2
3
4
5
List<Map> rows = em.createNamedQuery("XXX").list();
for(Map row : rows) {
byte[] bytes = (byte[]) row.get("features");
String features = new String(bytes, "UTF-8");
}
这样的话,Java变更features就是正常的中文,就可以直接回传给前端页面了。
Integer对象之间比较的坑对于Integer var=?在-128至127之间的赋值,Integer 对象是在IntegerCache.cache产生,会复用已有对象,这个区间内的Integer值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象。因此建议Integer对象在比较时应使用equals方法。
ArrayList的subList()方法返回值的坑ArrayList的subList方法返回的结果不可强转成ArrayList,否则会抛出ClassCastException异常:
1
java.util.RandomAccessSubList cannot be cast to java.util.ArrayList;
因为subList方法返回的是ArrayList的内部类SubList,并不是ArrayList,而是ArrayList的一个视图,对于内部类SubList的所有操作最终会反映到原列表上。
Arrays.asList()方法返回值的坑使用工具类Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法,它的 add/remove/clear方法会抛出UnsupportedOperationException异常。因为asList方法的返回对象是一个Arrays内部类,并没有实现集合的修改方法。Arrays.asList体现的是适配器模式,只是转换接口,后台的数据仍是数组。
1
2
3
4
String[] str = new String[] { "a", "b" };
List list = Arrays.asList(str);
list.add("c"); // 运行时异常。
str[0]= "gujin"; // 则list.get(0)也会随之修改。
1
2
3
4
5
6
7
8
9
10
java.lang.NullPointerException
at org.eclipse.jdt.internal.junit4.runner.SubForestFilter.shouldRun(SubForestFilter.java:81)
at org.junit.internal.runners.JUnit4ClassRunner.filter(JUnit4ClassRunner.java:110)
at org.junit.runner.manipulation.Filter.apply(Filter.java:47)
at org.junit.internal.requests.FilterRequest.getRunner(FilterRequest.java:34)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestLoader.createFilteredTest(JUnit4TestLoader.java:77)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestLoader.createTest(JUnit4TestLoader.java:68)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestLoader.loadTests(JUnit4TestLoader.java:43)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:444)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:678)
原因分析:新版Eclipse在启动单元测试用例时调用的是JUnit新版本的方法,即Eclipse已不支持junit4.9以下的版本。
解决办法:升级JUnit依赖版本即可
1
2
3
4
5
6
7
8
9
10
# 将tomcat安装成window后台服务:
service.bat install tomcat8-cza
# 卸载服务
service.bat remove tomcat8-cza
# 配置JVM内存参数
cd <tomcat_home>/bin
tomcat8w.exe //MS/tomcat8-cza
# 在桌面右下角可以看到tomcat8w.exe的服务管理图标,点击"配置",选择"java"标签,即可修改JVM参数。
1
echarts.getMap('福州').geoJson.features.forEach(function(item){console.log('"' + item.properties.name + '"' + ": [" + item.properties.cp + "],")});
]]>转载请注明出处:cloudnoter.com
系统要求:PHP5.3.2+以上版本
学习参考:Composer官方文档
Composer安装分两种:
将composer.phar文件内嵌于PHP应用目录下,命令如下:
1
2
3
4
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php -r "if (hash_file('SHA384', 'composer-setup.php') === 'e115a8dc7871f15d853148a7fbac7da27d6c0030b848d9b3dc09e2a0388afed865e6a3d6b3c0fad45c48e2b5fc1196ae') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
php composer-setup.php
php -r "unlink('composer-setup.php');"
以上命令分别做如下几件事件:
- 下载安装器
- 校验
- 执行安装
- 删除安装器
安装完成后,会在当前工作目录下生成可执行文件:composer.phar,你可以通过如下方式使用(运行)composer来进行项目的依赖管理:
1
2
php composer.phar <command>
# command指install等composer命令
将可执行二进制文件放在系统PATH路径下,命令如下:
1
2
3
4
# 假设局部安装中生成的composer.phar文件在当前目录下
# /usr/local/bin/目录是一个现成的PATH目录,
# 你也可以将可执行文件放置于其他PATH目录下
mv composer.phar /usr/local/bin/composer
通过这种方式,你就可以直接按如下方式来使用composer:
1
2
composer <command>
# command指install等composer命令
每个基于Composer的项目都需要包含composer.json文件,该文件用于声明项目所依赖的第三方库,其为JSON文件,格式类似:
1
2
3
4
5
{
"require": {
"monolog/monolog": "1.2.*"
}
}
require属性是用于声明依赖信息的地方。
当运行composer install安装依赖时,Composer会将依赖库下载到项目根目录的vendor目录下。如monolog依赖安装后,其存放在vendor\monolog\monolog目录下。
当执行如下命令安装依赖后,Composer将会在项目目录下创建composer.lock文件。
1
composer install
Composer将把安装时确切的版本号列表写入
composer.lock文件。这将锁定改项目的特定版本。
后续再次运行composer install时,Composer将先检查目录下是否有composer.lock,若有则直接忽略composer.json,而使用composer.lock中的确切的版本信息。团队成员可以共享该lock文件以解决版本不一致问题。
当依赖版本有升级时,若想更新依赖至最新版本可以运行如下命令
1
2
3
4
# 全部更新
composer update
# 只更新某个依赖库
composer update monolog/monolog [...]
对于库的自动加载信息,Composer 生成了一个vendor/autoload.php文件。通过引入这个文件,就实现了自动加载功能。
1
require 'vendor/autoload.php';
通过自动加载功能我们可以很容易的使用第三方代码。例如:项目依赖monolog,我们就可以像这样开始使用这个类库,并且他们将被自动加载。
1
2
3
4
$log = new Monolog\Logger('name');
$log->pushHandler(new Monolog\Handler\StreamHandler('app.log', Monolog\Logger::WARNING));
$log->addWarning('Foo');
当然,我们也可以在composer.json的autoload字段中增加自己的autoloader。
1
2
3
4
5
{
"autoload": {
"psr-4": {"Acme\\": "src/"}
}
}
Composer将注册一个PSR-4 autoloader到Acme命名空间。
你可以定义一个从命名空间到目录的映射。此时src会在你项目的根目录,与vendor文件夹同级。例如src/Foo.php文件应该包含Acme\Foo类。
添加autoload字段后,你应该再次运行install命令来生成 vendor/autoload.php文件。
引用这个文件也将返回autoloader的实例,你可以将包含调用的返回值存储在变量中,并添加更多的命名空间。这对于在一个测试套件中自动加载类文件是非常有用的,例如:
1
2
$loader = require 'vendor/autoload.php';
$loader->add('Acme\\Test\\', __DIR__);
不重复造轮子,这是大伙天天喊的,因为社区已经为大伙提供了很多可直接引用的轮子,这些轮子的学名就叫"库"。如果你觉得自己的项目可以帮到别人,你可以选择将其打包成库,并大告天下。你只要按如下步骤操作就行:
- composer.json中还有一个
version属性,但一般不建议设置,因为composer会根据tag标签自行推算版本号,如果项目代码为master,则版本会被推算为dev-master- packagist可理解为一个公共的组件仓库,类似maven中央库
- 对于未发布至packagist库的组件,引用方需要指定
repositories
比如,假如我们将项目发布至Github下,项目的composer.json如下:
1
2
3
4
5
6
{
"name": "simiam/composer-demo",
"require": {
"monolog/monolog": "1.0.*"
}
}
接下来,我们的另一个项目blog需要引用上面发布的simiam/composer-demo组件,则blog项目的composer.json内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
{
"name": "simiam/blog",
"repositories": [
{
"type": "vcs",
"url": "https://github.com/monkeychen/simiam"
}
],
"require": {
"simiam/composer-demo": "dev-master"
}
}
因为我们发布的是master分支,所以
require中依赖的版本号为dev-master
如果组件已经发布至packagist的话,则不需要声明repositories,因为composer默认会从中央库中搜索。
更详细的信息,可以参考这里。
]]>转载请注明出处:cloudnoter.com
/etc/apache2/httpd.conf/Library/WebServer/Documents/,一个是用户目录下的Sites目录(推荐使用),默认未开启;1
2
3
启动:sudo apachectl start
停止:sudo apachectl stop
重启:sudo apachectl restart
Step 1. 在用户目录下用Finder创建 Sites 文件夹;
Step 2. 在/etc/apache2/users/目录下添加 username.conf文件(username要替换成真正的用户名,下同):
1
2
cd /etc/apache2/users
sudo vim username.conf
Step 3. 在username.conf文件中添加如下内容:
username.conf内容
Step 4. 检查username.conf文件的权限是否正确,正确的应该为:
1
-rw-r--r-- 1 root wheel 126 Mar 23 23:02 username.conf
如果不是,则需要修改权限,使用如下命令:
1
sudo chmod 644 username.conf
Step 5. 修改httpd.conf文件配置:
1
sudo vim /etc/apache2/httpd.conf
在httpd.conf找到如下3行,并确保这3行的注释#是被删除的
1
2
3
LoadModule authz_core_module libexec/apache2/mod_authz_core.so
LoadModule authz_host_module libexec/apache2/mod_authz_host.so
LoadModule userdir_module libexec/apache2/mod_userdir.so
接着启用用户目录配置,同为删除对应行的#
1
Include /private/etc/apache2/extra/httpd-userdir.conf
Step 6. 修改httpd-userdir.conf文件配置
1
sudo vim /etc/apache2/extra/httpd-userdir.conf
取消如下行的#
1
Include /private/etc/apache2/users/*.conf
Step 7. 重启Apache,并检查配置是否生效
1
sudo apachectl restart
在浏览器输入:http://localhost/~username/,看是否配置成功
Step 8. 让apache支持php脚本
1
sudo vim /etc/apache2/httpd.conf
1
LoadModule php5_module libexec/apache2/libphp5.so
1
sudo apachectl restart
1
<?php phpinfo(); ?>
http://localhost/~username 如果能显示php环境信息,则说明php环境搭建成功
Step 9. 配置虚拟主机(vhost)
#去掉1
#Include /private/etc/apache2/extra/httpd-vhosts.conf
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
<VirtualHost *:80>
ServerAdmin admin@simiam.vhost.com
DocumentRoot "/Users/chenzhian/workspace/php/website/public"
ServerName simiam.vhost.com
DirectoryIndex main.php index.php index.html
<Directory "/Users/chenzhian/workspace/php/website/public">
Options FollowSymLinks Multiviews
MultiviewsMatch Any
AllowOverride All
Require all granted
</Directory>
ErrorLog "/private/var/log/apache2/simiam.vhost.com-error_log"
CustomLog "/private/var/log/apache2/simiam.vhost.com-access_log" common
</VirtualHost>
<VirtualHost *:80>
ServerAdmin admin@100.vhost.com
DocumentRoot "/Users/chenzhian/workspace/php/website/100"
ServerName 100.vhost.com
DirectoryIndex index.php index.html
<Directory "/Users/chenzhian/workspace/php/website/100">
Options FollowSymLinks Multiviews
MultiviewsMatch Any
AllowOverride All
Require all granted
</Directory>
ErrorLog "/private/var/log/apache2/100.vhost.com-error_log"
CustomLog "/private/var/log/apache2/100.vhost.com-access_log" common
</VirtualHost>
Step 1. 为了开启php的一些扩展功能,有必要对php.ini进行修改。OSX默认提供的php是没有php.ini文件的,因此我们需要自己创建一个。可以在/etc/目录下创建php.ini
1
sudo cp /etc/php.ini.default /etc/php.ini
如果不知道php默认是到哪里找php.ini文件的话,则使用命令php --ini:
命令输出如下类似信息:
1
2
3
4
Configuration File (php.ini) Path: /etc
Loaded Configuration File: /etc/php.ini
Scan for additional .ini files in: /Library/Server/Web/Config/php
Additional .ini files parsed: (none)
Step 2. 在php.ini文件最后添加如下内容以启用xdebug扩展:
1
2
3
4
5
6
7
8
9
10
11
12
[xdebug]
zend_extension=/usr/lib/php/extensions/no-debug-non-zts-20121212/xdebug.so
xdebug.remote_autostart=on
xdebug.remote_enable=on
xdebug.remote_enable=1
xdebug.remote_mode="req"
xdebug.remote_log="/var/log/xdebug.log"
xdebug.remote_host=127.0.0.1
xdebug.remote_port=9000
xdebug.remote_handler="dbgp"
xdebug.idekey="PhpStorm"
xdebug.remote_host的值建议设置为127.0.0.1,而不要设置为localhost(当开启调试模式时,可能会出现域名解析很慢的问题)
Step 1. 配置php解释器:直接在**Preferences的Languages->PHP**页面添加php命令路径:
1
/usr/bin/php
Step 2. 配置debug
在**PHP->Debug->DBGp**中添加如下信息:
1
2
3
IDE key:PhpStorm (与php.ini中xdebug配置项xdebug.idekey一致)
Host:localhost (apache服务地址)
Port:80 (apache服务端口)
]]>转载请注明出处:cloudnoter.com
客户现场的监控系统中有一个网络听诊器功能,其每隔1分钟会对全网设备进行ping操作,以此来尽可能快的发现设备及网络是否出现异常。暂且不说通过该功能来对设备及网络作健康检测是否靠谱。由于JAVA对于网络层以下的协议是无能为力的,而ping操作涉及ICMP与ARP协议,因此监控系统只能借助JNI机制来搞定。
监控系统的java.exe进程每隔几个小时就异常退出
PS:如果能早点想起步骤3,那就不用浪费步骤2的功夫了。
JNI异常导致java进程中止的原因可能为
由于linux环境下有这么一个机制:当内核检测到进程的物理内存不断增加至某一个值时,内核会直接将该进程kill掉。
windows是否也有这样的机制呢?目前尚未查证,还请高手解答。
在没有进一步证据的前提下,只能先猜测是否为进程物理内存出了问题,于是监测了下应用进程的物理内存损耗量,果然是缓慢递增的,但JVM堆内存仍然一切正常,由此大约知道是堆外内存使用上出了问题。
关于堆外内存的相关知识,可参考下面的文章:
至此,可以知道该问题与JAVA没啥关系了,但为了彻底搞明白,我还是硬着头皮找来DLL的C源码,想看看是否可以用我helloworld级别的C水平把这个问题搞定。
分析C/C++应用的内存,大伙一般都会想到perftool,可惜windows环境下我始终编译不过。于是谷歌上再搜索一把"windows内存泄漏",发现知乎上有文章推荐了一堆,但我要么下载不到,要么看不懂。最后是根据《C/C++内存泄漏及检测》介绍的方法定位到是dll中有一段代码使用了缓存导致内存泄漏,当内存达到JVM中设置的MaxDirectMemorySize值时,dll就会出现内存访问异常错误,最终导致java.exe进程异常退出了。
PS:在定位堆外内存异常相关问题时,为了快速重现问题,可以将MaxDirectMemorySize改小,MaxDirectMemorySize的默认值可认为与-Xmx设置的值一样(严格上不是,参见JVM源码分析之堆外内存完全解读)
该问题并非通用性问题,写这篇文章主要是为了记录下当时解决该问题的整个定位过程,文中一些知识点可能表述有误,还请批评指正。
]]>转载请注明出处:cloudnoter.com
所有的Laravel路由规则都定义在app/Http/routes.php文件中,Laravel框架在初始化时将自动加载该文件。Laravel中最基本的路由规则如下:
Route::get('foo', function () { return 'Hello World';});其接受两个参数:匹配该路由的URI、路由处理闭包Closure;该闭包定义了具体的路由规则。
默认路由文件
默认情况下,routes.php中可以定义简单的路由,或者将这些路由定义在路由组中。路由组将利用web中间件为定义在其中的路由规则提供’会话状态’与CSRF保存等功能。
任何未放在路由组中的路由规则将无权访问会话,也无法享受web中间件提供的CSRF保护等特性,因此如果您定义的路由规则需要web中间件提供的这些特性时,你需要确保将这些路由规则放入路由组中;通过情况下,我们会将大部分的路由规则都放在路由组中。
Route::group(['middleware' => ['web']], function () { //});Route类中可用的路由方法
框架路由引擎允许你注册如下的路由规则来响应相应的HTTP请求动作:
Route::get($uri, $callback);Route::post($uri, $callback);Route::put($uri, $callback);Route::patch($uri, $callback);Route::delete($uri, $callback);Route::options($uri, $callback);有时你需要注册一个路由规则来响应多个HTTP请求动作,因此你可以使用Route类的match方法来满足该需求。甚至你可以使用Route类的any方法来响应所有的HTTP请求动作。
Route::match(['get', 'post'], '/', function () { //});Route::any('foo', function () { //});Laravel中路由规则配置中的URL中允许设置参数(占位符),便于闭包或控制器方法提取与引用。Laravel的路由URL中参数占位符配置方式分两种:必选与可选。
这种机制蛮适合开发符合RESTful规范的应用,这里有一篇由《黑客与画家》、《软件随想录》译者阮一峰写的介绍RESTful概念的文章: 理解RESTful架构
必选参数占位符
所谓必选,即HTTP请求的URL中参数占位符的部分必需有具体的数据,否则该路由规则不会被匹配到。比如,你可能需要从请求URL中获取用户的ID时,你就可以定义如下的路由规则:
Route::get('user/{id}', function ($id) { return 'User '.$id;});// 当请求URL为http://somesite/user/2时,2将被提取出来并赋值给闭包函数中的变量id由上面的例子可知,路由参数占位符是由一对花括号来定义的。当然,我们也可以在URL中定义多个参数占位符,如下:
Route::get('posts/{post}/comments/{comment}', function ($postId, $commentId) { //});注意:路由参数占位符的定义需要符合PHP变量命名规范,如不能包含"-"符。
可选参数占位符
如果想让路由参数占位符是可选的(有时请求URL中的占位符部分可能是空的),此时可以在占位符名称的后面加上一个问号"?"即可。
Route::get('user/{name?}', function ($name = null) { return $name;});Route::get('user/{name?}', function ($name = 'John') { return $name;});当URL参数占位符设置为可选时,后面的闭包函数的参数需要提供默认值。
命名路由将允许你方便的生成URL或重定向URL,这些生成的URL最终将匹配该路由规则。你可通过如下方式定义命名路由:
Route::get('profile', ['as' => 'profile', function () { //}]);闭包函数的第二个数组参数的元素键值需要指定为"as",元素值即为路由规则名。当然,你也可以为控制器的方法来代替上面的闭包函数,如下:
Route::get('profile', [ 'as' => 'profile', 'uses' => 'UserController@showProfile']);定义命名路由还有如下方式:
Route::get('user/profile', 'UserController@showProfile')->name('profile');即先定义一个普通路由,然后再调用该路由规则实例的name方法。
路由组与命名路由
如果你正在使用路由规则组时,你可以在路由规则组定义的属性数组中添加一个key为"as",值为某字符串的元素,该元素的值将作为该路由组中包含的路由名字的前缀。如下:
Route::group(['as' => 'admin::'], function () { Route::get('dashboard', ['as' => 'dashboard', function () { // Route named "admin::dashboard" }]);});生成匹配命名路由的URL
一旦你已经为某一路由起了相应的名字,那么你就可以通过全局函数route来生成相应的URL:
// Generating URLs...$url = route('profile');// Generating Redirects...return redirect()->route('profile');如果命名路由规则的URL部分包含参数占位符,则你可以将参数值作为route函数的第二个数组参数的元素传入,框架将自动用该参数值代替占位符以生成相应的URL。
Route::get('user/{id}/profile', ['as' => 'profile', function ($id) { //}]);$url = route('profile', ['id' => 1]);]]>转载请注明出处:cloudnoter.com