KMP字符串匹配算法

KMP是一种不太常用的字符串匹配算法,但是在某些情况下,依然可以发挥作用,而它的next思想,更是可以应用在别的地方。

这是我第三次总结KMP算法,第一次是在ACM集训后填坑期间,第二次是在学《数据结构》补充笔记的时候,因为三次的目的不同,这次的侧重点应该是KMP算法本身,希望写出来的总结能对周老师和大家有些许帮助。

朴素算法

在说KMP算法前,必然要说朴素算法,也就是我们常用的字符串匹配算法。

假设我们有一个S串和一个T串,我们需要判断S串中是否包含一个T串,或者找到T串首次出现的位置等操作。一般情况下,我们会进行如下的操作:

public class Main {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        char[] S = scanner.next().toCharArray();
        char[] T = scanner.next().toCharArray();

        boolean isFind = false;
        for(int i = 0;i < S.length;i++){

            isFind = true;
            for(int j = 0, ti = i;j < T.length;j++, ti++){
                
                if(ti == S.length){
                    isFind = false;
                    break;
                }
                
                if(S[ti] != T[j]){
                    isFind = false;
                    break;
                }
            }

            if(isFind){
                break;
            }
        }

        System.out.println(isFind ? "YES" : "NO");
    }
}

当然,如果硬要说这种情况不好还想进行优化的话,可以进行优化的地方还是很多的,KMP便是针对指针回溯进行优化的算法。

什么是指针回溯

如上文的两个for循环,其中的 i,ti 和 j 分别对 S 和 T 串进行位置标记,然后进行比较等操作,我们可以把 i,ti 和 j 看做三个标记指针,而在每一轮的比较中(也就是内循环break前算作一轮比较),ti 和 j 进行不断向后移动进行比较,然而如果比较失败(发现了不同),那么,ti 就会回溯到 i + 1 的位置(当本次内循环结束,再次执行内循环时 i 已经进行了自增操作。),而 j 指针会回溯到 0 (也就是T串的开始位置),然后进行再次比较,以此重复直到找到匹配位置或者外循环结束。

如果看算法流程的话,可以更明显的看出指针回溯。

算法流程

  1. i 指向S串开始
  2. j 指向T串开始,ti 指向和 i 相同的位置
  3. 对 j 指向的字符和 ti 指向的字符判断 相同?执行4:执行6
  4. j 指针后移,ti指针后移
  5. j 或 ti 指向 S 或 T 串的结尾 ? 执行6:执行3
  6. i 自增
  7. i 指向S串的结束?执行8:执行2
  8. 打印结果

改良方向假设

很明显,如果我们可以做到两个指针减少回溯,或者保证一个不回溯,是不是就可以减少无用的匹配。

为了方便说明,我们可以举一个极端的栗子:

对于S串和T串是上图的样子,如果使用朴素算法,在匹配失败的时候,i指针(现在指向S串的第10个元素'a')应该会回溯到S串的第4个元素 'b' 上,而 j 指针,也会从T串的第8个元素'c',回溯到T串的第1个元素'a'上,然后进行重新循环比较。

而我们很明显可以看出来,这并不是最佳方案,最佳方案应该是i指针稳住不动,继续指向第10个元素,而j指针回溯到第1个元素上。因为很明显,i在前10个元素处不会有全匹配。

当然,这是我们已经可以一眼看到全局而能做出的最佳方案,而程序是不可能眼观全局的,但不意味着程序做不出这样的判断。

我们假设程序虽然不知道S串后面是什么样子,但已经知道了 T 串的构造和 T 串与 S 串已经匹配的部分,那么就应该可以做到清楚的知道指向 T 串的 j 指针应该怎么移动而可以避免回溯指向 S 串的 i 指针。

那么,如果上面说的不是神话,则明白 T 串的构造便是实现上述想法的关键。

因此,KMP算法诞生了。

KMP算法

终于讲到正题了,正如上文所说,与朴素算法相比,KMP算法多了对 T 串的构造进行分析,从而诞生了next数组。而next数组,便是整个KMP算法的核心,不仅仅是字符串匹配,在前缀,后缀,循环节判断等方面,next也能发挥重大作用。

next数组的意义

要想清楚什么是next数组,next数组的意义,切入点便是next数组在匹配中起到的作用。

还是这张图,朴素算法的缺点是什么呢,就是说:匹配过的就是做的无用功,不管你匹配多么长,都是按照没匹配过,无情的从开头再来一遍,这就导致了时间的浪费

如果我们匹配过以前的,不再放弃了,而是利用匹配过的信息推导出有用的信息。目的就是:匹配过的 S 串中还有多少直接等于 T 串的 ,假设知道了这个信息,是不是将会节省不少事件。

因此,next中就是保存的匹配过的 S 串中还有多少直接等于 T 串的

假设我们开始匹配的时候i指针指向了S[e],j指针指向T[0],进行了k次比较之后发现不同,也就是比对失败了,我们是不是可以得出结论:S[e,e+k] = T[0,k]

而如果我们的next数组已经记录了匹配过的S串中还有多少直接等于T串的,假设这个值为t,那么是不是也就意味着:S[e',e'+k] = T[0,t]。

如下图所示:

利用意义上的 next 数组得出 kmp 算法

定义: 此字符串的开头到某一位置称为这个字符串的前缀,,从某一个位置到这个字符串的最后位置称为这个字符串的后缀。

于是,我们需要截取所有的前缀来匹配后缀,因为 T 串在匹配时可能会用到各式各样的前缀,我们 next[]数组就是为了,一个前缀一个最大前后缀匹配值。

因此,求next数组的代码就出来了:

private int[] getNext(){
    char[] chars = this.str.toCharArray();
    int len = chars.length;
    int[] next = new int[len + 1];
    int k = -1, i = 0;
    next[0] = -1;

    while(i < len){
        if(k == -1 || chars[k] == chars[i]){
            i++;
            k++;
            next[i] = k;
        }else{
            k = next[k];
        }
    }
    return next;
}

(这是从今天的KMP题目SDUTOJ 2784中直接取出来的一个方法)

有了next数组后,当失配时,固定 i 指针不动,直接利用 T 串的 next 值滑,将我们知道的信息(我们知道到哪里的前缀和后缀相等)再次验证一遍来浪费时间。

那么我们的匹配算法就可以写作下面这个样子:

public class Main {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        char[] S = scanner.next().toCharArray();
        char[] T = scanner.next().toCharArray();

        boolean isFind = false;
        int j = 0;
        for(int i = 0;i < S.length;i++){
            
            if(j == T.length){

                isFind = true;
                break;
            }
            if(S[i] == T[j]){
                i++;
                j++;
            }else{
                j = next[j];
            }
        }

        System.out.println(isFind ? "YES" : "NO");
    }
}

由两个for循环嵌套变成一个是不是很爽的样子!!

当然,这只是next的常规用法(字符串匹配),而next的灵活用法还有很多(比如刚才说的前缀后缀什么的,还包括SDUTOJ 2784)。

结束语

这篇文章只是KMP总结和next数组的介绍,作为SDUTOJ 2784解题报告的前置技能 (:з」∠)

文中插图/定义引用了王政(@9cai)同学在队期间写的《Kmp 算法引论》(好流弊的名字^_^),因为Linux下做图片麻烦,也懒得去做图片了。

文章写成仓促,重点放在KMP与朴素算法比较和next的引入上,可能文章中存在些许问题,欢迎指正。

最后,引用我第二次对KMP总结文的内容作为结束语:

  1. 传统算法(朴素算法)最坏情况是O(nm),但一般是在O(n+m),也不是太坏,所以一般没有特殊要求的程序还在用这种算法;
  2. KMP算法在有大量重复的情况下优化明显,对主串较大时比较有效;
  3. next函数还是有缺陷的,比如模式串’aaaab’与主串’aabaaaab’匹配时,用时几乎和朴素算法一样;
  4. 以发展的眼光看问题,其实作为常用算法之一,匹配算法也有发展,KMP已经不是主流。
2015/11/16 01:19 am posted in  算法&数据结构

Linux下常用的dd命令

今天在微博看到了关于服务器硬盘负载的测试(原文见:使用 dd 命令进行硬盘 I/O 性能检测),前一段时间在搭建Minecraft服务器,我就经常怀疑IO爆表,但是内存可以用free,CPU和内存可以用top,可IO怎么查,我怎么知道这台服务器到底是哪里爆表了。 文章里提出用dd进行硬盘写入操作,通过写入情况分析现在负载到了一个什么程度。

但是,我并不太敢认可这种做法,这种做法就像判断瓶子满没满,再向里面注入写就知道了。但我目前并没有其他能看出磁盘当前性能的办法,暂且认可。因为如果瓶子已经满了,这些注入岂不是又增加负担。 也许我的担忧是在我无法负担更好的VPS的基础上的吧,就像好多朋友已经现在游戏服务器上玩的happy,我想看下服务器IO当前性能,结果一个dd就把服务器搞崩了,全线玩家集体掉线,也是挺无语。

继续说dd命令,之前看过一篇关于介绍新手级命令的文章(原文见:对 Linux 新手有用的 20 个命令),因为已经接触linux有一段时间了,几乎对这些命令用了不知多少遍了,唯独dd,之前就用过一次,就是帮同学写镜像,好像还写砸了。 dd的作用事把指定的输入文件拷贝到指定的输出文件中,并且在拷贝的过程中可以进行格式转换。现在就说下dd的常用用法。 先说下dd的语法和参数:

  • if =输入文件(或设备名称)。
  • of =输出文件(或设备名称)。
  • ibs = bytes 一次读取bytes字节,即读入缓冲区的字节数。
  • skip = blocks 跳过读入缓冲区开头的ibs*blocks块。
  • obs = bytes 一次写入bytes字节,即写 入缓冲区的字节数。
  • bs = bytes 同时设置读/写缓冲区的字节数(等于设置obs和obs)。
  • cbs = bytes 一次转换bytes字节。
  • count = blocks 只拷贝输入的blocks块。
  • conv = ASCII 把EBCDIC码转换为ASCII码。
  • conv = ebcdic 把ASCII码转换为EBCDIC码。
  • conv = ibm 把ASCII码转换为alternate EBCDIC码。
  • conv = blick 把变动位转换成固定字符。
  • conv = ublock 把固定们转换成变动位
  • conv = ucase 把字母由小写变为大写。
  • conv = lcase 把字母由大写变为小写。
  • conv = notrunc 不截短输出文件。
  • conv = swab 交换每一对输入字节。
  • conv = noerror 出错时不停止处理。
  • conv = sync 把每个输入记录的大小都调到ibs的大小(用ibs填充)。

1.对硬盘(西数黑盘)进行测试:

hypo@Hypo-TP:~$ dd if=/dev/zero of=test.img bs=512M count=4 oflag=dsync
记录了4+0 的读入
记录了4+0 的写出
2147483648字节(2.1 GB)已复制,19.7395 秒,109 MB/秒
hypo@Hypo-TP:~$ dd if=/dev/zero of=test.img bs=1G count=1 oflag=dsync
记录了1+0 的读入
记录了1+0 的写出
1073741824字节(1.1 GB)已复制,9.12763 秒,118 MB/秒

可以看到写入延时和写入速度。 其中/dev/zero事unix和linux下设计的白洞,可以源源不断的提供null字节,而也有一个unix和Linux的黑洞,那就是/dev/null。 2.写iso镜像:

sudo dd if=archlinux-2015.03.01-dual.iso of=/dev/sdd && sync
记录了1218560+0 的读入
记录了1218560+0 的写出
623902720字节(624 MB)已复制,0.910181 秒,685 MB/秒

3.备份整个磁盘,比如:

dd if=/home of=home.img bs=4M

4.备份MBR 备份: 备份磁盘开始的512Byte大小的MBR信息到指定文件:

dd if=/dev/hdx of=/path/to/image count=1 bs=512

恢复: 将备份的MBR信息写到磁盘开始部分:

dd if=/path/to/image of=/dev/hdx

5.修复硬盘 当硬盘较长时间(比如一两年年)放置不使用后,磁盘上会产生magnetic flux point。当磁头读到 这些区域时会遇到困难,并可能导致I/O错误。当这种情况影响到硬盘的第一个扇区时,可能导致 硬盘报废。下面的命令有可能使这些数据起死回生。且这个过程是安全,高效的。

dd if=/dev/sda of=/dev/sda

6.销毁磁盘数据 利用随机的数据填充硬盘:

dd if=/dev/urandom of=/dev/hda1

在某些必要的场合可以用来销毁数据。执行此操作以后,/dev/hda1将无法挂载,创建和拷贝操作无法执行。

2015/08/28 10:42 am posted in  Linux

依赖倒置原则

什么是依赖倒置原则

要给依赖倒置原则下个定义,三句话足以:

  • 高层模块不应该依赖低层模块,两者都应该依赖其抽象
  • 抽象不应该依赖细节
  • 细节应该依赖抽象

刚读这三句话的时候,我也有些恍惚,体会了好久,又想了好久才有体会。最近刚刚完成一个项目的Demo版,体会了下这个原则,然后想想不依照这个原则的后果,重写的心都有。 先解释一下这几个名词:高层模块和低层模块比较好区分,每个逻辑的实现都是由更小的原子逻辑组成,不可分割的原子逻辑就是低层模块,而原子逻辑的再组装就是高层模块。 至于抽象和细节,在Java中,可以理解为抽象就是指接口或抽象类,两者都是不能直接被实例化。而细节就是实现类,实现接口或继承抽象类而产生的类就是细节。细节是可以直接被实例化,也就是可以加上一个new之后便可以产生一个对象。 依赖倒置原则在Java语言中的表现就是:

  • 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的;
  • 接口或抽象类不依赖于实现类
  • 实现类依赖接口或抽象类

可以精简的定义为:面向接口编程。 采用依赖导致原则可以减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。 举一个栗子来具体说明下。

提高系统的稳定性

如果我们有一个司机类和一个奔驰类,来实现让司机开奔驰,我们只需要实现一个奔驰类:

package net.ihypo.dip;

public class Benz {
	public void run(){
		System.out.println("奔驰跑2333");
	}
}

并让奔驰可以跑,就可以了。 然后我们再实现这个司机类:

package net.ihypo.dip;

public class Driver {
	public void driver(Benz benz){
		benz.run();
	}
}

在使用的时候,把奔驰类对象传给司机类对象,这样就可以了:

package net.ihypo.dip;

public class Client {
	public static void main(String[] args) {
		Driver blf2 = new Driver();
		Benz benz = new Benz();
		blf2.driver(benz);
	}
}

功能实现了,就可以兴匆匆去上线发布了,其实明显有问题嘛,其实我刚刚完成Dome版的这个项目就是这么办的,忘了仔细设计未来的功能扩充。 问题来了,如果这个司机土豪又买了一个宝马,换句话说,项目需求改了,功能要求更高了,怎么办,最直接的方法就是重载Driver中的driver函数,当然,这只是因为增加了一个宝马,如果在来几辆车呢? 所谓“危难时刻见真情”,移植到技术上就是“变更才显真功夫”,业务需求变更永无休止,技术前进就永无止境,在发生变更时才能发觉我们的设计或程序是否耦合。明显,上面的程序耦合性过高导致要修改已经完成的代码。 者明显导致系统的可维护性降低,可读性降低,稳定性降低。什么是稳定性?固话的、健壮的才是稳定的,这里只是增加一个车类就需要修改司机类,这不是稳定性,只是易变性。被依赖者的变更竟然让依赖者来承担修改的成本,这样的依赖关系谁肯承担! 设计是否具备稳定性,只要适当地“松松土”,观察“设计的蓝图”是否还可以茁壮地成长就可以得出结论,稳定性较高的设计,在周围环境频繁变化的时候,依然可以做到“我自岿然不动”。 那么这个程序应该怎么改进?这就用到了依赖倒置原则的第一条高层模块不应该依赖低层模块,两者都应该依赖其抽象。 所以应该先有一个司机的抽象:

package net.ihypo.dip;

public interface IDriver {
	void driver(ICar car);
}

以及一个汽车的抽象:

package net.ihypo.dip;

public interface ICar {
	void run();
}

这样,让抽象之间发生关系,让高层模块不一来低层模块。几口只是一个抽象概念,是对一类事物的最抽象描述,具体的实现代码由相应的实现类来完成。 然后分别实现IDriver和ICar来具体化司机类和汽车类,并实现功能。

package net.ihypo.dip;

public class Benz implements ICar{
	@Override
	public void run(){
		System.out.println("奔驰跑2333");
	}
}


package net.ihypo.dip;

public class Driver implements IDriver{
	@Override
	public void driver(ICar car) {
		// TODO Auto-generated method stub
		car.run();
	}
}

Driver只是传入了ICar接口,至于是哪个具体的类型,需要在高层模块中声明:

package net.ihypo.dip;

public class Client {
	public static void main(String[] args) {
		Driver blf2 = new Driver();
		ICar benz = new Benz();
		blf2.driver(benz);
	}
}

这样,即使需求改变了,也不需要修改即成的部分,提高系统的稳定性。

降低并行开发引起的风险

如果在开始的那个栗子中,如果甲乙两个人同时开发这个程序,甲负责司机类,乙负责汽车类,那么如果甲开发的速度比较快,那么如果甲开发完之后能不能进行测试阶段呢?并不能,因为乙还不没有开发完,并没有数据参数让你来测试。 但是用了接口就不一样了,接口在一开始就是定好的,就像一个契约,接口所定的东西就是未来参数多实现的东西,完全不用在乎乙开发的如何,因为他所写的也是符合这个契约的。 有一个JMock工具,其最基本的功能就是根据抽象虚拟出一个对象进行测试,测试类代码如下:

public class DriverTest extends TestCase{
	Mockery context = new JUnit4Mockery();

	@Test
	public void testDriver(){
		final ICar car = context.mock(ICar.class);
		IDriver driver = new Driver();

		context.checking(new Expectations(){{
			oneOf(car).run();
		}});
		driver.driver(car);
	}
}

我们只需要一个ICar接口就可以独立的对Driver类进行单元测试。 两个相互依赖的对象可以分别进行开发,孤立地进行单元测试,进而保证并行开发的效率和质量,TDD开发的精髓不就如此么,车市驱动开发,先写好单元测试类,然后再写实现类。 抽象是对实现的约束,对依赖者而言,也是一种契约,不仅仅约束自己,还同时约束自己与外部的关系,其目的是保证所有的细节不脱离不了这个圈圈,始终让你的对象做到“言必信,行必果”。

最佳实践

依赖倒置原则的本质就是通过抽象使各个类或模块的实现彼此独立,不相互影响,实现模块间的松耦合。怎么在项目中使用这个规则呢?只要遵循下面的几个规则就可以:

  • 每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备
  • 变量的表面类型尽量是接口或者抽象类:当然只是尽量,比如如果使用clone方法就必须要使用实现类,这是JDK的规范
  • 任何类都不应该从具体类派生:如果一个项目处于开发状态确实不应该出现从具体类派生出子类的情况,但这不是绝对的,因为人都是会犯错误的,有时候设计缺陷是在所难免的,因此只要不超过两层的继承都是可以忍受的。
  • 尽量不要覆写基类方法
  • 结合里氏替换原则

依赖倒置原则的优点在小型项目中很难体现出来,例如小于10人月的项目,使用简单的SSH框架,基本上不话费力气就可以完成,是否采用依赖倒置原则影像不大。 但是在一个中大型项目中,采用依赖倒置原则有非常多的有点,特别是规避一些非技术因素引起的问题,通过采用接口等抽象对实现类进行约束可以减少需求变化引起的工作量剧增的情况。 依赖倒置原则是6个设计原则中最难实现的原则,它是实现开闭原则的重要途径,依赖倒置原则没有实现,就别想实现对拓展开发,对修改关闭。 但是,依赖倒置原则不是万能的,现实世界中的确存在必须依赖细节的事物,比如法律,就必须依赖细节的定义。 我们在实际的项目中使用依赖倒置原则时需要审时度势,不要抓住一个原则不放,每个原则的邮电都是有限度的,并不是放之四海皆准的真理,所以别为了遵循一个原则而放弃了一个项目的终极目标:投产上线和盈利!

2015/06/30 10:08 am posted in  设计模式

Dockerfile基本结构与指令

Dockerfile是一个文本格式的配置文件,用户可以使用Dockerfile快速创建自定义的镜像。

基本结构

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

# This dockerfile uses the ubuntu image
# VERSION 2 - EDITION 1
# Author: docker_user
# Command format: Instruction [arguments / command] ..

# Base image to use, this must be set as the first line
FROM ubuntu

# Maintainer: docker_user  (@docker_user)
MAINTAINER docker_user docker_user@email.com

# Commands to update the image
RUN echo "deb http://archive.ubuntu.com/ubuntu/ raring main universe" >> /etc/apt/sources.list
RUN apt-get update && apt-get install -y nginx
RUN echo "\ndaemon off;" >> /etc/nginx/nginx.conf

# Commands when creating a new container
CMD /usr/sbin/nginx

其中,一开始必须指明所基于的镜像名称,接下来推荐说明维护者信息。 后面则是镜像操作指令,例如RUN指令,RUN指令将对镜像执行跟随的命令。每运行一条 RUN 指令,镜像添加新的一层,并提交。最后是 CMD指令,来指定运行容器时的操作命令。

指令

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

FROM

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

MAINTAINER

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

RUN

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

ENV

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

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

COPY

格式为 COPY
复制本地主机的 (为 Dockerfile 所在目录的相对路径)到容器中的
当使用本地目录为源目录时,推荐使用 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 指令,后续命令如果参数是相对路径,则会基于之前命令指定的路径。例如

WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd

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

ONBUILD

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

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

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

FROM image-A

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

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

2015/06/07 10:27 am posted in  Docker

Docker网络基础

大量的互联网应用服务包括多个服务组件,这往往需要多个容器之间通过网络通信进行相互配合。 Docker目前提供了映射容器端口到宿主主机和容器互联机制来为容器提供网络服务。

端口映射实现访问容器

在启动容器的时候,如果不指定对应参数,在容器外部是无法通过网络来访问容器内的网络应用和服务的。

从外部访问容器应用

当容器中运行一些网络应用,要让外部访问这些应用时,可以通过-P或-p参数来指定端口映射。当使用-P标记时,Docker会随即映射49000~49900中的端口至容器内部开放的网络端口。 小写-p则可以指定要映射的端口,并且,在一个指定端口上只可以帮顶一个容器。支持的格式有:

  • ip:hostPort:containerPort
  • ip::containerPort
  • hostPort:containerPort

映射所有接口地址

使用hostPort:containerPort格式将本地5000端口映射到容器5000端口(之前创建私有仓库的例子):

docker run -d -p 5000:5000 registry

这时默认会帮顶本地所有接口上的所有地址。可以多次使用-p参数从而映射多个端口。

映射到指定地址的指定端口

可以使用ip:hostPort:containerPort格式指定映射使用一个特定地址,比如locakhost地址127.0.0.1。

docker run -d -p 127.0.0.1:5000:5000 registry

映射到指定地址的任意端口

可以使用ip::containerPort格式帮顶localhost的任意端口到容器的5000端口,本地主机会自动分配一个端口:

docker run -d -p 127.0.0.1::5000 registry

可以使用udp标记来指定udp端口:

docker run -d -p 127.0.0.1:5000:5000/udp registry

查看端口映射配置

可以使用docker port命令来查看当前映射的端口配置,也可以查看绑定的地址:

docker port registry 
5000/tcp -> 0.0.0.0:5000
docker port registry 5000
0.0.0.0:5000

容器有自己的内部网络和IP地址(使用docker inspect + ID可以获取所有变量值)。

容器互联实现容器间通信

容器的连接(linking)系统是除了端口映射外另一种可以与容器中应用进行交互的方式。他会在源和接收容器之间创建一个隧道,接受容器可以看到源容器指定的信息。

容器互联

使用--link参数可以让容器之间安全的进行交互。 比如创建一个数据库容器:

docker run --name dbserver mysql

让后创建一个容器,并将这个容器连接到dbserver容器:

docker run -d --name likeweb --link dbserver:dbserver ubuntu

因为要连接的容器并没有启动,所以建立完容器之后会报错,这里只是演示--link,请忽略。 --link参数的格式为--link name:alias,其中name是要链接的容器的名称,alias是这个连接的别名。 可以用docker ps 命令查看连接情况。 这样Docker两个容器之间创建了一个安全隧道而不需要开放外部端口,避免了数据库端口到外部网络上。 Docker通过两种方式为容器公开连接信息:

  • 环境变量
  • 更新/etc/hosts文件

可以使用env命令来查看容器的环境变量。 Docker目前采用了Linux系统自带的网络系统来实现对网络服务的支持,这既可以利用现有成熟的技术提供稳定的支持,又可以实现快速的高性能转发。

2015/06/06 10:27 am posted in  Docker