Zookeeper学习笔记

简介

Zookeeper 分布式服务框架是 Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。

Zookeeper的数据存储模型:ZK会维护一个具有层次关系的数据结构,它非常类似于一个标准的文件系统。
Zookeeper的数据存储模型

Zookeeper 数据结构有如下这些特点:

  1. 每个子目录项如 NameService 都被称作为 znode,这个 znode 是被它所在的路径唯一标识,如 Server1 这个 znode 的标识为 /NameService/Server1
  2. znode 可以有子节点目录,并且每个 znode 可以存储数据,注意 EPHEMERAL 类型的目录节点不能有子节点目录
  3. znode 是有版本的,每个 znode 中存储的数据可以有多个版本,也就是一个访问路径中可以存储多份数据
  4. znode 可以是临时节点,一旦创建这个 znode 的客户端与服务器失去联系,这个 znode 也将自动删除,Zookeeper 的客户端和服务器通信采用长连接方式,每个客户端和服务器通过心跳来保持连接,这个连接状态称为 session,如果 znode 是临时节点,这个 session 失效,znode 也就删除了
  5. znode 的目录名可以自动编号,如 App1 已经存在,再创建的话,将会自动命名为 App2
  6. znode 可以被监控,包括这个目录节点中存储的数据的修改,子节点目录的变化等,一旦变化可以通知设置监控的客户端,这个是 Zookeeper 的核心特性,Zookeeper 的很多功能都是基于这个特性实现的

Znode目录节点类型:

  1. PERSISTENT:持久化目录节点,这个目录节点存储的数据不会丢失;
  2. PERSISTENT_SEQUENTIAL:顺序自动编号的目录节点,这种目录节点会根据当前已近存在的节点数自动加 1,然后返回给客户端已经成功创建的目录节点名;
  3. EPHEMERAL:临时目录节点,一旦创建这个节点的客户端与服务器端口也就是 session 超时,这种节点会被自动删除;
  4. EPHEMERAL_SEQUENTIAL:临时自动编号节点

适用场景

统一命名服务(Name Service)

分布式应用中,通常需要有一套完整的命名规则,既能够产生唯一的名称又便于人识别和记住,通常情况下用树形的名称结构是一个理想的选择,树形的名称结构是一个有层次的目录结构,既对人友好又不会重复。说到这里你可能想到了 JNDI,没错 Zookeeper 的 Name Service 与 JNDI 能够完成的功能是差不多的,它们都是将有层次的目录结构关联到一定资源上,但是 Zookeeper 的 Name Service 更加是广泛意义上的关联,也许你并不需要将名称关联到特定资源上,你可能只需要一个不会重复名称,就像数据库中产生一个唯一的数字主键一样。
Name Service 已经是 Zookeeper 内置的功能,你只要调用 Zookeeper 的 API 就能实现。如调用 create 接口就可以很容易创建一个目录节点。

配置管理(Configuration Management)

配置的管理在分布式应用环境中很常见,例如同一个应用系统需要多台 PC Server 运行,但是它们运行的应用系统的某些配置项是相同的,如果要修改这些相同的配置项,那么就必须同时修改每台运行这个应用系统的 PC Server,这样非常麻烦而且容易出错。
像这样的配置信息完全可以交给 Zookeeper 来管理,将配置信息保存在 Zookeeper 的某个目录节点中,然后将所有需要修改的应用机器监控配置信息的状态,一旦配置信息发生变化,每台应用机器就会收到 Zookeeper 的通知,然后从 Zookeeper 获取新的配置信息应用到系统中。
配置管理结构图

集群管理(Group Membership)

Zookeeper 能够很容易的实现集群管理的功能,如有多台 Server 组成一个服务集群,那么必须要一个“总管”知道当前集群中每台机器的服务状态,一旦有机器不能提供服务,集群中其它集群必须知道,从而做出调整重新分配服务策略。同样当增加集群的服务能力时,就会增加一台或多台 Server,同样也必须让“总管”知道。
Zookeeper 不仅能够帮你维护当前的集群中机器的服务状态,而且能够帮你选出一个“总管”,让这个总管来管理集群,这就是 Zookeeper 的另一个功能 Leader Election。
它们的实现方式都是在 Zookeeper 上创建一个 EPHEMERAL 类型的目录节点,然后每个 Server 在它们创建目录节点的父目录节点上调用 getChildren(String path, boolean watch) 方法并设置 watch 为 true,由于是 EPHEMERAL 目录节点,当创建它的 Server 死去,这个目录节点也随之被删除,所以 Children 将会变化,这时 getChildren上的 Watch 将会被调用,所以其它 Server 就知道已经有某台 Server 死去了。新增 Server 也是同样的原理。
Zookeeper 如何实现 Leader Election,也就是选出一个 Master Server。和前面的一样每台 Server 创建一个 EPHEMERAL 目录节点,不同的是它还是一个 SEQUENTIAL 目录节点,所以它是个 EPHEMERAL_SEQUENTIAL 目录节点。之所以它是 EPHEMERAL_SEQUENTIAL 目录节点,是因为我们可以给每台 Server 编号,我们可以选择当前是最小编号的 Server 为 Master,假如这个最小编号的 Server 死去,由于是 EPHEMERAL 节点,死去的 Server 对应的节点也被删除,所以当前的节点列表中又出现一个最小编号的节点,我们就选择这个节点为当前 Master。这样就实现了动态选择 Master,避免了传统意义上单 Master 容易出现单点故障的问题。
集群管理结构图

共享锁(Locks)

共享锁在同一个进程中很容易实现,但是在跨进程或者在不同 Server 之间就不好实现了。Zookeeper 却很容易实现这个功能,实现方式也是需要获得锁的 Server 创建一个 EPHEMERAL_SEQUENTIAL 目录节点,然后调用 getChildren方法获取当前的目录节点列表中最小的目录节点是不是就是自己创建的目录节点,如果正是自己创建的,那么它就获得了这个锁,如果不是那么它就调用 exists(String path, boolean watch) 方法并监控 Zookeeper 上目录节点列表的变化,一直到自己创建的节点是列表中最小编号的目录节点,从而获得锁,释放锁很简单,只要删除前面它自己所创建的目录节点就行了。

队列管理

Zookeeper 可以处理两种类型的队列:

  1. 当一个队列的成员都聚齐时,这个队列才可用,否则一直等待所有成员到达,这种是同步队列。
  2. 队列按照 FIFO 方式进行入队和出队操作,例如实现生产者和消费者模型。
    同步队列用 Zookeeper 实现的实现思路如下:
    创建一个父目录 /synchronizing,每个成员都监控标志(Set Watch)位目录 /synchronizing/start 是否存在,然后每个成员都加入这个队列,加入队列的方式就是创建 /synchronizing/member_i 的临时目录节点,然后每个成员获取 / synchronizing 目录的所有目录节点,也就是 member_i。判断 i 的值是否已经是成员的个数,如果小于成员个数等待 /synchronizing/start 的出现,如果已经相等就创建 /synchronizing/start。

    集群配置

    zoo.cfg
    1
    2
    3
    4
    initLimit=5 
    syncLimit=2
    server.1=192.168.211.1:2888:3888
    server.2=192.168.211.2:2888:3888
    说明:
  • initLimit:这个配置项是用来配置 Zookeeper 接受客户端(这里所说的客户端不是用户连接 Zookeeper 服务器的客户端,而是 Zookeeper 服务器集群中连接到 Leader 的 Follower 服务器)初始化连接时最长能忍受多少个心跳时间间隔数。当已经超过 10 个心跳的时间(也就是 tickTime)长度后 Zookeeper 服务器还没有收到客户端的返回信息,那么表明这个客户端连接失败。总的时间长度就是 5*2000=10 秒
  • syncLimit:这个配置项标识 Leader 与 Follower 之间发送消息,请求和应答时间长度,最长不能超过多少个 tickTime 的时间长度,总的时间长度就是 2*2000=4 秒
  • server.A=B:C:D:其中 A 是一个数字,表示这个是第几号服务器;B 是这个服务器的 ip 地址;C 表示的是这个服务器与集群中的 Leader 服务器交换信息的端口;D 表示的是万一集群中的 Leader 服务器挂了,需要一个端口来重新进行选举,选出一个新的 Leader,而这个端口就是用来执行选举时服务器相互通信的端口。如果是伪集群的配置方式,由于 B 都是一样,所以不同的 Zookeeper 实例通信端口号不能一样,所以要给它们分配不同的端口号。

除了修改 zoo.cfg 配置文件,集群模式下还要配置一个文件 myid,这个文件在 dataDir 目录下,这个文件里面就有一个数据就是 A 的值,Zookeeper 启动时会读取这个文件,拿到里面的数据与 zoo.cfg 里面的配置信息比较从而判断到底是那个 server。


观点仅代表自己,期待你的留言。

https://www.ibm.com/developerworks/cn/opensource/os-cn-zookeeper/

Redis集群备注

介绍

  1. Redis集群是一个可以在多个Redis节点之间进行数据共享的设施(installation)。
  2. Redis集群不支持那些需要同时处理多个键的Redis命令,因为执行这些命令需要在多个Redis节点之间移动数据,并且在高负载的情况下,这些命令将降低Redis集群的性能,并导致不可预测的行为。
  3. Redis集群通过分区(partition)来提供一定程度的可用性(availability):即使集群中有一部份分节点失效或者无法进行通讯,集群也可以继续处理命令请求。

Redis集群提供了以下两个好处:
1、将数据自动切分(split)到多个节点的能力。
2、当集群中的一部份节点失效或者无法进行通讯时,仍然可以继续处理命令请求的能力。

数据节点分配

Redis 集群没有并使用传统的一致性哈希来分配数据,而是采用另外一种叫做哈希槽 (hash slot)的方式来分配的。redis cluster 默认分配了 16384 个slot,当我们set一个key 时,会用CRC16算法来取模得到所属的slot,然后将这个key 分到哈希槽区间的节点上,具体算法就是:CRC16(key) % 16384。集群中的每个节点负责处理一部分哈希槽。

这种将哈希槽分布到不同节点的做法使得用户可以很容易地向集群中添加或者删除节点。
当添加集群节点时,需要将已有节点的哈希槽移动到新的节点上进行处理。
当删除集群节点时,需要先将节点上已分配的哈希槽移动到其它的节点上再进行删除。

客户端存储分配

当客户端向集群节点中任一节点发出存储或读取请求时,redis节点先根据KeyHash出来的值判断是否属于当前集群节点能进行处理,如果不能处理则会将能完成这一请求的Redis节点信息返回给客户端。客户端将再次向能处理请求的Redis节点发出请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ redis-cli -c -p 7000
redis 127.0.0.1:7000> set foo bar
-> Redirected to slot [12182] located at 127.0.0.1:7002
OK
redis 127.0.0.1:7002> set hello world
-> Redirected to slot [866] located at 127.0.0.1:7000
OK
redis 127.0.0.1:7000> get foo
-> Redirected to slot [12182] located at 127.0.0.1:7002
"bar"
redis 127.0.0.1:7000> get hello
-> Redirected to slot [866] located at 127.0.0.1:7000
"world"

集群节点故障

采用数据分片的处理集群数据分配就存在无法避免的问题,当其中一个集群节点挂掉时,分配到此节点上哈希槽的数据将无法处理。
为了使得集群在一部分节点下线或者无法与集群的大多数(majority)节点进行通讯的情况下, 仍然可以正常运作,
Redis 集群对每个集群节点都提供了了主从复制功能: 集群中的每个节点都有 1 个至 N 个复制品(replica), 其中一个复制品为主节点(master), 而其余的 N-1 个复制品为从节点(slave)。


观点仅代表自己,期待你的留言。

Redis实现分布式锁和分布式队列

分布式锁

通过Redis做分布式系统的共享内存实现方案中,可以实现分布式锁的功能。实现方法就是用 SET NX 命令设置一个设定了存活周期 TTL 的 Key 来获取锁,通过删除 Key 来释放锁,通过存活周期来保证避免死锁。
如:SET resource_name my_random_value NX PX 30000

注:


SET key value [EX seconds] [PX milliseconds] [NX|XX]
从2.6.12版本开始,redis为SET命令增加了一系列选项:

  • EX seconds – Set the specified expire time, in seconds.
  • PX milliseconds – Set the specified expire time, in milliseconds.
  • NX – Only set the key if it does not already exist.
  • XX – Only set the key if it already exist.
  • EX seconds – 设置键key的过期时间,单位时秒
  • PX milliseconds – 设置键key的过期时间,单位时毫秒
  • NX – 只有键key不存在的时候才会设置key的值
  • XX – 只有键key存在的时候才会设置key的值

但,以上简单方案存在一个单点故障的风险。在master/slave环境下,当:

  1. 客户端A获取到锁.
  2. master节点在将 key 复制到 slave 节点之前崩溃了
  3. 此时,slave节点被提升为master节点.
  4. 客户端 B 从新的 master 节点获得了锁(而这个锁实际上已经由客户端 A 所持有),导致了系统中有两个客户端在同一时间段内持有同一个互斥锁,破坏了互斥锁的安全性。

__解决方法:__需要解决以上问题,需要在进行锁设值时将惟一标识系统的ID做为value一同存储到Redis中,在完成所有的操作后,进行解锁时取出Redis内存储的value进行比对,
如果锁key对应的value还是当前系统的ID,那么表示当前锁目前只被当前系统所持有,反之,则表示锁被其它的系统抢占,那么需要回滚所有的操作。

如:

客户端A(ID为SYSA) Redis 客户端B(ID为SYSB)
获取到锁(SET dist_lock SYSA NX PX 30000)
进行其它的业务操作。。。 master发生故障,slave被提升为NewMaster(未将master中dist_lock复制到NewMaster)
获取到锁(SET dist_lock SYSB NX PX 30000)
解锁 (get dist_lock,对value进行判定,为SYSA则执行解锁,否则回滚业务)
解锁 (get dist_lock,对value进行判定,为SYSB则执行解锁,否则回滚业务)

分布式队列

Redis实现分布式队列主要实现是通过有序集合实现,通过ZADD向集合内添加元素,同时添加多个元素时为事务处理(不存在部分成功部分失败的情况)

注:


ZADD key [NX|XX] [CH] [INCR] score member [score member ...]
从3.0.2版本开始,增加以下选项:

  • XX: Only update elements that already exist. Never add elements.
  • NX: Don’t update already existing elements. Always add new elements.
  • CH: Modify the return value from the number of new elements added, to the total number of elements changed (CH is an abbreviation of changed). Changed elements are new elements added and elements already existing for which the score was updated. So elements specified in the command line having the same score as they had in the past are not counted. Note: normally the return value of ZADD only counts the number of new elements added.
  • INCR: When this option is specified ZADD acts like ZINCRBY. Only one score-element pair can be specified in this mode.

另外的选择

_Java库:_其实也在使用redisson来实现分布式锁。它封装了针对Redis各种操作的分布式实现。

其它语言可参照官网文章: http://redis.io/topics/distlock?cm_mc_uid=77610277652214581184982&cm_mc_sid_50200000=1464593765


观点仅代表自己,期待你的留言。

关于MariaDB事务阻塞等待及锁的实验笔记

实验环境

Server Version: 10.0.24-MariaDB
SET GLOBAL tx_isolation=’REPEATABLE-READ’;
SET SESSION tx_isolation=’REPEATABLE-READ’;
SET @@autocommit=0;

SHOW VARIABLES like ‘autocommit’;
SELECT @@GLOBAL.tx_isolation, @@tx_isolation;

实验结论

幻读的概念是另一事务先完成的情况下判断

事务A 事务B
start TRANSACTION;
select * from t1;(查询出2条数据)
start TRANSACTION;
insert into t1 values(null,’v222’);
select * from t1;(查询出2条数据)
commit;(这一步很关键,它标识了事务B的完成)
select * from t1;(查询出3条数据)

InnoDB存储引擎在REPEATABLE-READ的隔离级别下解决了幻读情况,所以会造成以上的情况。

并发事务修改同一行数据时,后执行更新的事务会__阻塞等待__先执行更新事务先结束(rollback或commit)或者超时

事务A 事务B
start TRANSACTION; start TRANSACTION;
update t1 SET da=’v1111’ where id=1;
update t1 SET da=’v1111’ where id=1;(此时会等待事务A执行完成 OR 等待超时)

并发事务更新时只要被更新的数据存在交集,那么就会存在__阻塞等待__另一事务完成的现象

事务A 事务B
start TRANSACTION;
select * from t1;(查询出2条数据)
start TRANSACTION;
insert into t1 values(null,’v222’);
update t1 set da=’updated’(此时会等事务B的完成)
commit;

并发事务更新时只要存在全表扫描(不管数据存在不存在交集),那么就会存在__阻塞等待__另一事务完成的现象

事务A 事务B
start TRANSACTION;
select * from t1;(查询出2条数据)
start TRANSACTION;
update t1 SET da=’v1113333331’ where id=1
update t1 set da=’updated’ where da like ‘%0000%’(此时会等事务B的完成)
commit;

观点仅代表自己,期待你的留言。

数据库事务隔离级别与并发事务控制(悲观锁与乐观锁)

事务特性

事务具有4个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为ACID特性。

  1. 原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
  2. 一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束。
  3. 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
  4. 持久性(Durability):一个事务一旦提交,他对数据库的修改应该永久保存在数据库中。

事务的隔离级别

由于事务的隔离性允许多个事务同时处理同一数据,所以,在多个事务并发操作的过程中,如果控制不好隔离级别,就有可能产生_脏读_、_不可重复读_或者_幻读_等现象。因此在操作数据的过程中需要合理利用数据库锁机制或者多版本并发控制机制获取更高的隔离等级,但是,随着数据库隔离级别的提高,数据的并发能力也会有所下降。所以,如何在并发性和隔离性之间做一个很好的权衡就成了一个至关重要的问题。

ANSI/ISO SQL定义的标准隔离级别从高到低依次为:可串行化(Serializable)、可重复读(Repeatable reads)、提交读(Read committed)、未提交读(Read uncommitted)。

隔离级别 脏读(Dirty Read) 不可重复读(NonRepeatable Read) 幻读(Phantom Read)
未提交读(Read uncommitted) 可能 可能 可能
提交读(Read committed) 不可能 可能 可能
可重复读(Repeatable Read) 不可能 不可能 可能
可串行化(Serializable) 不可能 不可能 不可能

===========================================================================================

  • 未提交读(Read Uncommitted):允许脏读,也就是可能读取到其他会话中未提交事务修改的数据。
  • 提交读(Read Committed):被读取的数据可以被其他事务修改。这样就可能导致不可重复读。也就是说,事务的读取数据的时候获取读锁,但是读完之后立即释放(不需要等到事务结束),而写锁则是事务提交之后才释放。释放读锁之后,就可能被其他事物修改数据。Oracle等多数数据库默认都是该级别 (不重复读)
  • 可重复读(Repeated Read):所有被Select获取的数据都不能被修改,这样就可以避免一个事务前后读取数据不一致的情况。但是却没有办法控制幻读,因为这个时候其他事务不能更改所选的数据,但是可以增加数据,因为前一个事务没有范围锁。Mysql的InnoDB引擎默认隔离级别。
  • 可串行化(Serializable):所有事务都一个接一个地串行执行,这样可以避免幻读(phantom reads),每次读都需要获得表级共享锁,读写相互都会阻塞。

数据库并发控制

虽然将事务串形化可以保证数据在多事务并发处理下不存在数据不一致的问题,但串行执行使得数据库的处理性能大幅度地下降,常常是你接受不了的。所以,一般来说,数据库的隔离级别都会设置为read committed(只能读取其他事务已提交的数据),然后由应用程序使用__乐观锁/悲观锁__来弥补数据不一致的问题。

乐观锁

虽然名字中带“锁”,但是乐观锁并不锁住任何东西,而是在提交事务时检查自己上次读取这条记录后,是否有其他事务修改了这条记录,如果没有则提交,如果被修改了则回滚。如果并发的可能性并不大,那么锁定策略带来的性能消耗是非常小的。

常见实现方式:在数据表增加version字段,每次事务开始时将取出version字段值,而后在更新数据的同时version增加1(如:update xxx set data=#{data},version=version+1 where version=#{version}),如没有数据被更新,那么说明数据由其它的事务进行了更新,此时就可以判断当前事务所操作的历史快照数据。

悲观锁

和乐观锁相比,悲观锁则是一把真正的锁了,它通过SQL语句“select for update”锁住select出的那批数据,这时如果其他事务来更新这批数据时会等待。

总的来说,悲观锁相对乐观锁更安全一些,但是开销也更大,甚至可能出现数据库死锁的情况,建议只在乐观锁无法工作时才使用。


观点仅代表自己,期待你的留言。

Maven应用远程部署

简介

在程序开发的过程中对研发环境服务器应用部署将会非常的频繁,而通过tomcat-maven-plugin的deploy很容易实现web应用的远程发布。
而针对jar的发布一般会搭建maven私服,同样在研发阶段也会发布的非常频繁通过maven的deploy也非常容易实现maven私服的jar提交。

远程发布Web应用

以tomcat-maven-plugin为例,具体配置如下:
pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version='1.0' encoding='utf-8'?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<path>/</path>
<port>8080</port>
<uriEncoding>UTF-8</uriEncoding>
<server>DevServer</server>
<url>http://deploy.dev.com/manager</url>
</configuration>
</plugin>
</plugins>
</build>
</project>

说明:
server: 为settings.xml中配置的server节点ID,用于上传鉴权。
url:发布到的服务器的tomcat/manager工程访问地址。

settings.xml

1
2
3
4
5
6
7
8
9
10
11
<?xml version='1.0' encoding='utf-8'?>
<settings xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd" xmlns="http://maven.apache.org/SETTINGS/1.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<servers>
<server>
<id>DevServer</id>
<username>abc</username>
<password>xxx</password>
</server>
</servers>
</settings>

tomcat-users.xml

1
2
3
4
5
6
<?xml version='1.0' encoding='utf-8'?>
<tomcat-users>
<role rolename="manager-gui" />
<role rolename="manager-script" />
<user username="abc" password="xxx" roles="manager-gui,manager-script"/>
</tomcat-users>

–[更新] 发现更简便配置,详细如下:—
将maven的pom.xml中将server节点直接替换成username与password节点,这样就不需要在setting.xml中进行配置了。
pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version='1.0' encoding='utf-8'?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<path>/</path>
<port>8080</port>
<uriEncoding>UTF-8</uriEncoding>
<username>abc</username>
<password>xxx</password>
<url>http://deploy.dev.com/manager</url>
</configuration>
</plugin>
</plugins>
</build>
</project>

远程发布依赖库jar

pom.xml

1
2
3
4
5
6
7
8
9
10
11
<?xml version='1.0' encoding='utf-8'?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<distributionManagement>
<snapshotRepository>
<id>snapshots</id>
<name>libs-snapshot</name>
<url>http://mvn.code.com/artifactory/libs-snapshot-local</url>
</snapshotRepository>
</distributionManagement>
</project>

说明:
id: 表示配置的用户名和密码,这个ID在settings.xml里配置
url: 为私服上传地址。

settings.xml

1
2
3
4
5
6
7
8
9
10
11
<?xml version='1.0' encoding='utf-8'?>
<settings xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd" xmlns="http://maven.apache.org/SETTINGS/1.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<servers>
<server>
<id>snapshots</id>
<username>abc</username>
<password>xxx</password>
</server>
</servers>
</settings>

配置完成之后执行maven deploy就OK啦。


观点仅代表自己,期待你的留言。

Maven应用发布之不同环境不同配置的实现

简介

一个应用程序从研发到发布一般需要经过三套环境(研发环境、测试环境、生产环境)甚至更多。针对这同的运行环境应用程序也需要一些不同的参数配置值(如果日志输出级别、数据库连接池配置等)。
Maven的Profile配置很好的解决这一问题。在pom.xml中配置多套参数值,在程序打包__编译时期__通过部署环境对配置参数进行替换来完成。
__注意:__当配置项找不到对应的配置值时会保持原样。

pom.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
34
35
36
37
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<properties>
<catalina.base>/app/servers/logs</catalina.base>
</properties>
<profiles>
<profile>
<id>dev</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<catalina.log.priority>debug</catalina.log.priority>
</properties>
</profile>
<profile>
<id>test</id>
<properties>
<catalina.log.priority>warn</catalina.log.priority>
</properties>
</profile>
<profile>
<id>pro</id>
<properties>
<catalina.log.priority>error</catalina.log.priority>
</properties>
</profile>
</profiles>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
</project>

说明:
以上配置表示在编译时期对src/main/resources目录下配置文件中的${catalina.log.priority}${catalina.base}进行替换。而catalina.base参数值不区分部署环境。
编译开发环境的部署程序包命令如下:

1
Jianjun:~ Jianjun$ mvn package –P dev

编译测试环境的部署程序包命令如下:

1
Jianjun:~ Jianjun$ mvn package –P test

编译生产环境的部署程序包命令如下:

1
Jianjun:~ Jianjun$ mvn package –P pro

如果不指-P参数,默认会使用dev的配置,由于dev节点配置了__activeByDefault__

参数配置项太多

当配置项太多之后我们可以通过外置properties文件来进行配置,而properties文件需要与pom.xml在同一路径下。
properties配置如下:
product-deploy-config-dev.properties

1
catalina.log.priority=debug

product-deploy-config-test.properties

1
catalina.log.priority=warn

product-deploy-config-pro.properties

1
catalina.log.priority=error

pom.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
34
35
36
37
38
39
40
41
42
43
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<properties>
<catalina.base>/app/servers/logs</catalina.base>
</properties>
<profiles>
<profile>
<id>dev</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<build>
<filters>
<filter>product-deploy-config-dev.properties</filter>
</filters>
</build>
</profile>
<profile>
<id>test</id>
<build>
<filters>
<filter>product-deploy-config-test.properties</filter>
</filters>
</build>
</profile>
<profile>
<id>pro</id>
<build>
<filters>
<filter>product-deploy-config-pro.properties</filter>
</filters>
</build>
</profile>
</profiles>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
</project>

观点仅代表自己,期待你的留言。

使用端口转发解决80端口使用问题

简介

当我用在使用linux非root用户进行应用发布时,如遇到应用程序需要占用80端口时总会由于权限不足遇到java.net.BindException: Permission denied:80的错误报出。
Linux为保证系统安全,限制了非root用户对1024以下的端口进行占用。
那么,本文将通过端口转发的方式解决80端口在Linux和MacOSX系统上占用的问题。

linux非Root用户使用80端口

linux操作系统的可以通过iptables来进行端口转发,将流向80端口的数据内部转发到大于1024的端口上。那么非root用户就可以通过占用大于1024的端口进行程序应用的功能处理。
以80转发到8080端口为例:

1
2
3
4
5
6
[root@localhost ~]# sudo iptables -t nat -A PREROUTING -p tcp -m tcp --dport 80 -j REDIRECT --to-ports 8080
[root@localhost ~]# sudo iptables -t nat -A OUTPUT -d localhost -p tcp -m tcp --dport 80 -j REDIRECT --to-ports 8080
[root@localhost ~]# iptables -t filter -F
[root@localhost ~]# iptables-save
[root@localhost ~]# service iptables restart
[root@localhost ~]# chkconfig iptables on

iptables -t filter -F : 为清除所有的过滤规则。
chkconfig iptables on : 设置iptables开机启动。

iptables存储文件:/etc/sysconfig/iptables

MacOSX使用80端口

MacOS操作系统下则使用pf来进行端口转发。
pf启动时会自动装载/etc/pf.conf文件,因此,我们可以通过修改这个文件进行80端口数据的转发。
修改后的文件内容如下:

1
2
3
4
5
6
7
scrub-anchor "com.apple/*"
nat-anchor "com.apple/*"
rdr-anchor "com.apple/*"
rdr on lo0 inet proto tcp from any to 127.0.0.1 port 80 -> 127.0.0.1 port 8080
dummynet-anchor "com.apple/*"
anchor "com.apple/*"
load anchor "com.apple" from "/etc/pf.anchors/com.apple"

使用 pfctl使用重新加载pf.conf配置,并启动pf

1
2
Jianjun:~ Jianjun$ sudo pfctl -f /etc/pf.conf
Jianjun:~ Jianjun$ sudo pfctl -e

另,关闭pf的命令为:sudo pfctl -d


观点仅代表自己,期待你的留言。

docker-dockerfile

基本结构

Dockerfile 由一行行字母大写的指令语句组成,并且支持以 # 开头的注释行。
一般的,Dockerfile 分为四部分:基础镜像信息、维护者信息、镜像操作指令和容器启动时执行指令。

指令

指令的一般格式为 INSTRUCTION arguments,指令包括 FROM、MAINTAINER、RUN 等。

FROM

格式为 FROM <image>FROM <image>:<tag>
第一条指令必须为 FROM 指令。并且,如果在同一个Dockerfile中创建多个镜像时,可以使用多个 FROM 指令(每个镜像一次)。

MAINTAINER

格式为 MAINTAINER <name>,指定维护者信息。

RUN

格式为 RUN <command>RUN ["executable", "param1", "param2"]
前者将在 shell 终端中运行命令,即 /bin/sh -c;后者则使用 exec 执行。指定使用其它终端可以通过第二种方式实现,例如 RUN ["/bin/bash", "-c", "echo hello"]
每条 RUN 指令将在当前镜像基础上执行指定命令,并提交为新的镜像。当命令较长时可以使用 \ 来换行。

CMD

支持三种格式

  • CMD [“executable”,”param1”,”param2”] 使用 exec 执行,推荐方式;
  • CMD command param1 param2 在 /bin/sh 中执行,提供给需要交互的应用;
  • CMD [“param1”,”param2”] 提供给 ENTRYPOINT 的默认参数;
    指定启动容器时执行的命令,__每个 Dockerfile 只能有一条 CMD 命令__。如果指定了多条命令,只有最后一条会被执行。
    如果用户启动容器时候指定了运行的命令,则会覆盖掉 CMD 指定的命令。

    EXPOSE

格式为 EXPOSE <port> [<port>...]
告诉 Docker 服务端容器暴露的端口号,供互联系统使用。在启动容器时需要通过 -P,Docker 主机会自动分配一个端口转发到指定的端口。

ENV

格式为 ENV <key> <value>。 指定一个环境变量,会被后续 RUN 指令使用,并在容器运行时保持。
例如

1
2
3
4
ENV PG_MAJOR 9.3
ENV PG_VERSION 9.3.4
RUN curl -SL http://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgress && …
ENV PATH /usr/local/postgres-$PG_MAJOR/bin:$PATH

ADD

格式为 ADD <src> <dest>
该命令将复制指定的 <src> 到容器中的 <dest>。 其中 <src> 可以是Dockerfile所在目录的一个相对路径;也可以是一个 URL;还可以是一个 tar 文件(自动解压为目录)。

COPY

格式为 COPY <src> <dest>
复制本地主机的 <src>(为 Dockerfile 所在目录的相对路径)到容器中的 <dest>
当使用本地目录为源目录时,推荐使用 COPY

ENTRYPOINT

两种格式:

  • ENTRYPOINT [“executable”, “param1”, “param2”]
  • ENTRYPOINT command param1 param2(shell中执行)。
    配置容器启动后执行的命令,并且不可被 docker run 提供的参数覆盖。
    每个 Dockerfile 中只能有一个 ENTRYPOINT,__当指定多个时,只有最后一个起效__。

    VOLUME

格式为 VOLUME ["/data"]
创建一个可以从本地主机或其他容器挂载的挂载点,一般用来存放数据库和需要保持的数据等。

USER

格式为 USER daemon
指定运行容器时的用户名或 UID,后续的 RUN 也会使用指定用户。
当服务不需要管理员权限时,可以通过该命令指定运行用户。并且可以在之前创建所需要的用户,例如:RUN groupadd -r postgres && useradd -r -g postgres postgres。要临时获取管理员权限可以使用 gosu,而不推荐 sudo

WORKDIR

格式为 WORKDIR /path/to/workdir
为后续的 RUN、CMD、ENTRYPOINT 指令配置工作目录。
可以使用多个 WORKDIR 指令,后续命令如果参数是相对路径,则会基于之前命令指定的路径。
例如

1
2
3
4
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd

则最终路径为 /a/b/c

ONBUILD

格式为 ONBUILD [INSTRUCTION]
配置当所创建的镜像作为其它新创建镜像的基础镜像时,所执行的操作指令。
例如,Dockerfile 使用如下的内容创建了镜像 image-A

1
2
3
4
[...]
ONBUILD ADD . /app/src
ONBUILD RUN /usr/local/bin/python-build --dir /app/src
[...]

如果基于 image-A 创建新的镜像时,新的Dockerfile中使用 FROM image-A指定基础镜像时,会自动执行 ONBUILD 指令内容,等价于在后面添加了两条指令。

1
2
3
4
5
FROM image-A

#Automatically run the following
ADD . /app/src
RUN /usr/local/bin/python-build --dir /app/src

使用 ONBUILD 指令的镜像,推荐在标签中注明,例如 ruby:1.9-onbuild

创建镜像

编写完成 Dockerfile 之后,可以通过 docker build 命令来创建镜像。
基本的格式为 docker build [选项] 路径,该命令将读取指定路径下(包括子目录)的 Dockerfile,并将该路径下所有内容发送给 Docker 服务端,由服务端来创建镜像。因此一般建议放置 Dockerfile 的目录为空目录。也可以通过 .dockerignore文件(每一行添加一条匹配模式)来让 Docker 忽略路径下的目录和文件。
要指定镜像的标签信息,可以通过 -t 选项,
例如:

1
$ sudo docker build -t myrepo/myapp /tmp/test1/

https://yeasy.gitbooks.io/docker_practice/content/dockerfile/index.html

观点仅代表自己,期待你的留言。

docker数据卷与网络配置

数据卷

Docker 挂载数据卷的默认权限是读写,用户也可以通过 :ro 指定为只读。
示例:

1
$ sudo docker run -d -P --name web -v /src/webapp:/opt/webapp:ro training/webapp python app.py

创建数据卷

在用 docker run 命令的时候,使用 -v 标记来创建一个数据卷并挂载到容器里。在一次 run 中多次使用可以挂载多个数据卷。
示例:

1
$ sudo docker run -d -P --name web -v /webapp training/webapp python app.py

挂载一个主机目录作为数据卷

加载主机的 /src/webapp 目录到容器的 /opt/webapp 目录.
示例:

1
$ sudo docker run -d -P --name web -v /src/webapp:/opt/webapp training/webapp python app.py

注意:本地目录的路径必须是绝对路径,如果目录不存在 Docker 会自动为你创建它。

挂载一个本地主机文件作为数据卷

-v 标记也可以从主机挂载单个文件到容器中
示例:

1
$ sudo docker run --rm -it -v ~/.bash_history:/.bash_history ubuntu /bin/bash

删除数据卷

数据卷是被设计用来持久化数据的,它的生命周期独立于容器,Docker不会在容器被删除后自动删除数据卷,并且也不存在垃圾回收这样的机制来处理没有任何容器引用的数据卷。如果需要在删除容器的同时移除数据卷。可以在删除容器的时候使用 docker rm -v这个命令。无主的数据卷可能会占据很多空间,要清理会很麻烦。

数据卷容器

如果你有一些持续更新的数据需要在容器之间共享,最好创建数据卷容器。
数据卷容器,其实就是一个正常的容器,专门用来提供数据卷供其它容器挂载的。
首先,创建一个名为 dbdata 的数据卷容器:

1
$ sudo docker run -d -v /dbdatas --name dbdata training/postgres echo Data-only container for postgres

然后,在其他容器中使用 --volumes-from 来挂载 dbdata 容器中的数据卷。

1
2
$ sudo docker run -d --volumes-from dbdata --name db1 training/postgres
$ sudo docker run -d --volumes-from dbdata --name db2 training/postgres

可以使用超过一个的 –volumes-from 参数来指定从多个容器挂载不同的数据卷。 也可以从其他已经挂载了数据卷的容器来级联挂载数据卷。

1
$ sudo docker run -d --name db3 --volumes-from db1 training/postgres

*注意:使用 --volumes-from 参数所挂载数据卷的容器自己并不需要保持在运行状态。
如果删除了挂载的容器(包括 dbdata、db1 和 db2),数据卷并不会被自动删除。如果要删除一个数据卷,必须在删除最后一个还挂载着它的容器时使用 docker rm -v 命令来指定同时删除关联的容器。

利用数据卷容器来备份、恢复、迁移数据卷

备份

首先使用 –volumes-from 标记来创建一个加载 dbdata 容器卷的容器,并从主机挂载当前目录到容器的 /backup 目录。
命令如下:

1
$ sudo docker run --volumes-from dbdata -v $(pwd):/backup ubuntu tar cvf /backup/backup.tar /dbdata

容器启动后,使用了 tar 命令来将 dbdata 卷备份为容器中 /backup/backup.tar 文件,也就是主机当前目录下的名为 backup.tar 的文件。

恢复

如果要恢复数据到一个容器,首先创建一个带有空数据卷的容器 dbdata2。

1
$ sudo docker run -v /dbdata --name dbdata2 ubuntu /bin/bash

然后创建另一个容器,挂载 dbdata2 容器卷中的数据卷,并使用 untar 解压备份文件到挂载的容器卷中。

1
$ sudo docker run --volumes-from dbdata2 -v $(pwd):/backup busybox tar xvf /backup/backup.tar

为了查看/验证恢复的数据,可以再启动一个容器挂载同样的容器卷来查看

1
$ sudo docker run --volumes-from dbdata2 busybox /bin/ls /dbdata

网络配置

外部访问容器

docker容器一般使用5000做为服务端口(类似于80端口的概念)。

  • docker支持在docker run时增加-P来将主机随机端口(49000~49900)映射到容器内的5000。可通过docker ps查看到对应的占用端口。
  • docker支持在docker run时增加-p来将指定的主机端口映射到容器内的指定端口。
    支持以下格式:
    • ip:hostPort:containerPort: 将主机指定IP下的指定端口绑定到容器内的指定端口。
    • ip::containerPort:绑定主机指定IP下的任意端口到容器的指定端口,本地主机会自动分配一个端口。还可以使用 udp 标记来指定 udp 端口。如$ sudo docker run -d -p 127.0.0.1:5000:5000/udp training/webapp python app.py
    • hostPort:containerPort:指定主机所有IP下的指定端口到容器内的指定端口。

使用 docker port 来查看当前映射的端口配置,
注意:

  • 容器有自己的内部网络和 ip 地址(使用 docker inspect 可以获取所有的变量,Docker 还可以有一个可变的网络配置。)
  • -p 参数可以多次使用。如:$ sudo docker run -d -p 5000:5000 -p 3000:80 training/webapp python app.py

    容器互联

    连接系统依据容器的名称来执行。因此,首先需要通过--name自定义一个好记的容器命名。使用 docker ps 来验证设定的命名。

使用 --link 参数可以让容器之间安全的进行交互。参数的格式为 --link name:alias,其中 name 是要链接的容器的名称,alias 是这个连接的别名。
将web容器与db容器连接,且连接被命名为db, 启动命令如下:

1
$ sudo docker run -d -P --name web --link db:db training/webapp python app.py

实现原理:
Docker 通过 2 种方式为容器公开连接信息:

  • 环境变量。 通过linux命令env可以看到,环境变量通过增加alias做为前缀标识来区别容器的环境变量与连接。
  • 更新 /etc/hosts 文件。在hosts文件中增加alias条目的映射ip。

观点仅代表自己,期待你的留言。