第一个Dockerfile示例

Dockerfile创建一个镜像。
创建一个镜像最流行的方式是用Dockerfile文件。
因为容器内的进程一旦退出,容器也就没有运行的必要了。基于这一点,容器也会随着容器内进程的退出而退出。(这点好像跟一个指令ENTRYPOINT有关系,后续补充)。今天的目的是创建一个持续在运行的容器,那么就需要在容器里面运行一个不会退出的进程。写一个死循环的脚本。
脚本内容如下:

1
2
3
4
5
#!/bin/bash
while true;do
echo 'hello,docker';
sleep 2;
done;

然后修改一下权限:

1
chmod 777 start.sh

写一个文件,起名为Dockerfile

1
2
3
FROM ubuntu:latest
COPY start.sh /
CMD ./start.sh

然后把这两个文件放在同一个目录下,运行:

1
docker run --rm -t base:base .
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
注意别漏掉最后的`.`。
`-t`是为生成的镜像打一个`tag`,`--rm`是删除中间层。
命令产生的信息:
```shell
Sending build context to Docker daemon 3.072kB
Step 1/3 : FROM ubuntu:latest
---> 1d9c17228a9e
Step 2/3 : COPY start.sh /
---> 20b5113895c0
Step 3/3 : CMD ./start.sh
---> Running in 490de3666795
Removing intermediate container 490de3666795
---> 80f7c8ab8600
Successfully built 80f7c8ab8600
Successfully tagged base:base

然后从这个镜像运行一个容器:

1
docker run -d base:base

其中-d是以守护线程的方式运行一个容器。
进入容器查看:

1
docker exec -it xxxxx bash

其中xxxxx是容器的id,可以通过docker ps -a|grep base得到。
进入容器,运行ps -ef得到:

1
2
3
4
5
6
D        PID  PPID  C STIME TTY          TIME CMD
root 1 0 0 15:24 ? 00:00:00 /bin/sh -c ./start.sh
root 8 1 0 15:24 ? 00:00:00 /bin/bash ./start.sh
root 21 0 0 15:24 pts/0 00:00:00 bash
root 371 8 0 15:36 ? 00:00:00 sleep 2
root 372 21 0 15:36 pts/0 00:00:00 ps -ef

OK.

Docker中的容器

前面讲过,容器是镜像的一个运行示例,带有额外的可写文件层。镜像是只有可读层的。

创建容器

使用create命令创建容器。

1
docker create -it ubuntu:ttag

然后可以通过如下命令查看容器的创建情况:

1
docker ps -a

显示如下:

1
2
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                    PORTS               NAMES
dd3fae4a712a ubuntu:ttag "/bin/bash" 6 seconds ago Created clever_khorana

创建的容器是处于停止状态的。

新建并启动容器

1
docker run ubuntu:ttag /bin/echo 'Hello Docker'

显示如下:

1
Hello Docker

现在详细介绍以下docker run后台进行的动作:

  1. 检查本地是否存在对应的镜像,如果不存在,则从仓库下载;
  2. 利用镜像创建并启动一个容器;
  3. 分配一个文件系统,并在只读的镜像层外面挂载一层可读写层;
  4. 从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去;
  5. 从地址池中配置一个IP地址给容器;
  6. 执行用户制定的程序;
  7. 执行完毕后终止容器;
    启动一个可交互的容器:
    1
    docker run -it ubuntu:latest /bin/bash
    解释一下上面it参数的含义:
  • t参数:分配一个伪终端并绑定到容器的标准输入上;
  • i参数:让容器的标准输入打开;
    对于创建的容器,退出后,容器即终止。

    守护态运行容器

    添加-d参数即可:
    1
    docker run -d ubuntu:latest /bin/bash -c 'while true;do echo hello wolrd;sleep 2;done'
    这样容器就运行起来了,通过docker ps -a查看:
    1
    2
    CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                     PORTS               NAMES
    63eb0ed6ab1d ubuntu:latest "/bin/bash -c 'while…" 17 seconds ago Up 16 seconds nostalgic_edison
    但控制台是看不到输出的,要看到容器的输出信息,执行以下命令:
    1
    docker logs 63e
    63e是容器ID

    终止容器

    通过命令:
    1
    docker stop 63e
    此外还有docker startdocker restart命令,后者操作的是运行中的容器,即将容器终止然后重新start

    进入容器

    1
    docke exec -it 63e /bin/bash

    删除容器

    1
    docker rm 63e
    该命令带有几个参数:
  • -f:删除一个运行终端容器时,需要该参数,否则会出错。
  • -l:删除容器的连接,但保留容器。(这里尚有疑问,未完待续…)
  • -v:删除容器挂载的数据卷。

    容器的导入和导出

    跟镜像一样,可以导入导出。
    使用importexport
    1
    2
    #容器导出
    docker export 63e>test_for_run.tar
    这样就把容器导出到本地文件test_for_run.tar
    1
    2
    #容器导入
    cat test_for_run.tar |docker import - test/ubuntu:v1.0
    通过上面这条命令,就可以在本地镜像库看到多了一个镜像。即容器导入,导入会成为镜像。
    和导入镜像相比,导入容器生成的镜像体积要小,因为丢弃了所有的历史记录和元数据信息(tag等等),镜像导入产生的镜像则要大一些。

彻底搞懂时间问题

最近工作遇到了这个问题,之前反复整改,自己不够重视,这次终于吃了一次大亏。

timestamp

时间戳的定义:从197011000秒(UTC时间,及0时区)起至现在的总秒数。
只能一直存在的疑问是,时区问题,其实这个时间戳的概念是根本不存在时区问题的。全球各地的时间戳都是一样的。也就是说,从197011000秒(UTC时间,及0时区)开始,全球各地同时开始计数,所以这个数字不管到哪里都是一样。

UTC时间

这个就跟时区有关了。又称为协调世界时,和0时区的时间一致。

东八区

这个就是北京时间了。

JS中关于时间的一些转换

JS中一般都要用Date对象:

1
new Date();

这个在windows系统上,得到的时间是本机设定的时区时间。大家一般都是东八区。
得到时间戳:

1
new Date().getTime();

如果已经有了时间戳,可以这样得到对应的UTC时间:

1
new Date(xxxxxx).toISOString();

这样得到的时间格式是这样的:

1
"2019-01-09T16:37:34.662Z"

题外话:ES查询时,如果传入一个时间戳,他会根据你ES机器的时区进行转换(如果你数据存储的时候用的是utc时间),MDZZ。

Docker基础操作

类似于数据库,有增删改查,docker镜像也有这些操作。

镜像获取

1
docker pull ubuntu

这个命令可以把镜像从仓库中下载下来,那仓库的地址又是哪个呢?
默认地址是Docker Hub仓库,如果有需要修改仓库地址的需求,可以在/etc/docker/下面新建daemon.json文件,其内容变更如下:

1
2
3
{
"registry-mirrors": ["https://registry.docker-cn.com"]
}

Note:网址仅是示例。

上述pull命令拉下来的镜像默认是latest标签的镜像,如果想下载其他标签,例如14.04的镜像,可以使用以下命令:

1
docker pull ubuntu:14.04

镜像查看

docker images可以查看本地所有的镜像。
以下是输出:

1
2
3
4
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu 14.04 7e4b16ae8b23 10 days ago 188MB
ubuntu latest 1d9c17228a9e 10 days ago 86.7MB
ubuntu ttag 1d9c17228a9e 10 days ago 86.7MB

IMAGE ID唯一标识了镜像,TAG是标签,其他字段含义均符合字段名含义。这里有2个镜像的IMAGE ID是一样的,这是因为我刚刚通过命令为其中一个镜像打上了标签。打上标签,看着是2个镜像,其实底层是一个镜像。删除的时候,也仅仅是删除tag而已。直到该镜像的最后一个tag删除时,该镜像才会被删除。

镜像删除

2种删除方式。

根据tag删除。

1
docker rmi xxxxxx

其中xxxxx可以为标签或者ID

根据ID删除

使用docker rmi删除镜像时,会先尝试删除该镜像的所有标签,然后删除该镜像。
NOTE:当有基于该镜像创建的容器存在时,该镜像是无法被删除的,无论该容器是什么状态。
删除容器的命令:

1
docker rm xxxxx

其中xxxx是容器的id

创建容器

基于已有镜像的容器创建

1
2
3
docker run -it ubuntu:latest /bin/bash
touch test
exit

查看容器id

1
docker ps -a

找到id,这里为9xx937d9adf,然后根据这个容器创建一个新的镜像。

1
docker commit -m "create a new image " -a "volcanno" 9xx937d9adf test

现在就已经创建出来一个新的镜像,docker images查看:

1
2
3
4
5
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
test latest 17b8285f6baf 5 minutes ago 86.7MB
ubuntu 14.04 7e4b16ae8b23 10 days ago 188MB
ubuntu latest 1d9c17228a9e 10 days ago 86.7MB
ubuntu ttag 1d9c17228a9e 10 days ago 86.7MB

可以看到test仓库有了一个新镜像。

根据模板创建镜像

根据模板创建镜像,指的是根据操作系统模板文件在本地仓库导入一个镜像:

1
cat ubuntu-14.04-x86_64-minimal.tar.gz|docker import - ubuntu:14.04

镜像存入存出

存入存出指的是可以将镜像从本地文件的形式导入本地镜像库,或者从本地镜像库导出为本地文件,这样可以文件的形式进行迁移。

镜像导出

1
docker save -o ubuntu_14.04.tar ubuntu:latest

上面命令的含义是将本地镜像库中ubuntu:latest导出到本地,本地名字为ubuntu_14.04.tar

镜像导入

1
docker load < ubuntu_14.04.tar

镜像上传

从本地仓库上传到DockerHub官方仓库。

1
docker push ubuntu:latest

初识Docker

以我的理解,Docker类似于闭包,把一个应用或者其他的东西以及其需要的生存环境全部打包起来,使用该应用的时候,就不需要配置各种环境了,直接使用这个打包起来的东西就好。

优势(来自教科书):

  • 更快速的交付和部署;
  • 更搞笑的资源利用;
  • 更简单的更新管理;
  • 更轻松的迁移和扩展;

核心概念

1. image(镜像)

镜像,顾名思义,就是个镜子,专门用来复制出实例的。可以类比java语言中的class。不同的是image的作用是用来创建镜像实例,也就是container

2. container(容器)

容器是一个真正的运行着的活的东西。是根据image创建出来的。可以想象成一个正在运行的系统。

容器从镜像启动的时候,Docker会在镜像的最上层创建一个可写层。镜像本身 只读,因此镜像可以保持不变。

3. Respository(仓库)

集中存放image的场所。跟 github一样,分为公有和私有仓。镜像的分门别类是通过tag进行的。

未完待续…

懒加载单例模式

懒加载指的是,加载类的阶段不产生出单例对象,在运行时需要单例对象的时候再实例化出单例对象。

错误代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.company;

public class LazyClass {

private static LazyClass instance = null;

private LazyClass(){}

public static LazyClass getInstance(){
if (instance == null)
{
synchronized (LazyClass.class){
if (instance==null)
{
instance = new LazyClass();
}
}
}
return instance;
}
}

可以看到这段代码的单例对象instance没有加 volatile关键字,这就会导致如下问题。

在语句instance = new LazyClass()执行的时候,是分为3个步骤的:

  1. 内存中分配对象空间。
  2. 内存初始化为零值。
  3. 内存地址赋给instance引用。

值得注意的是,以上3个步骤执行的顺序是不定的。因为有指令重排序这一机制的存在。假如说,线程A执行到第一个判空之前停止,线程B进入同步代码块,然后执行上述三个步骤中的1,3,然后线程A获得执行权,那么B会判空失败,直接返回instance

究其原因,主要是指令重排序的锅。因此要禁止指令重排序。即,使用volatile修饰instance。这样就可以保证 1,2这2个步骤是绝对不会跑到3这个步骤后面执行的

正确代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.company;

public class LazyClass {

private static volatile LazyClass instance = null;

private LazyClass(){}

public static LazyClass getInstance(){
if (instance == null)
{
synchronized (LazyClass.class){
if (instance==null)
{
instance = new LazyClass();
}
}
}
return instance;
}
}

About

Volcanno

​ 我的名字是Volcanno VayneVayne是我喜欢的游戏中的一个角色的名字(虽然是女角色),所以取来用了,而Volcanno是某天突发灵感,自己创造了这个单词。

17年毕业,非专业程序员;

​ 目前在做程序员的工作;

​ 正在研究调用链追踪系统和ELK日志系统;

​ 初心是后端,然后目前团队内大家都偏向于全栈(手动狗头),导致我也开始偏向全栈了;

​ 哦,对了,我还是一个要成为海贼王的男人;


联系方式:huangbin艾特818.live.cn。

关于JAVA内部类的一些思考

四种内部类

Java中有四种内部类,静态内部类,非静态内部类,匿名内部类,局部类。

《Effectvie Java》中有一句话说的话,内部类存在的唯一唯一作用是为其外围类(enclosing class)服务。这是使用内部类的根本的立足点。

静态内部类和非静态内部类

​ 这2种内部类的区别在于,非静态内部类的实例,都拥有一个指向其外围类实例的引用。在大量应用的场合,非静态内部类就会存在大量指向外围类实例的指针,存在一定的资源浪费,当然这是相对的,在适合应用非静态内部类的场合,是不能摒弃非静态内部类的。只是我们应该有这么一个原则:能用静态内部类,就不要使用非静态内部类。

​ 立足于内部类的存在是为外围类服务的这一基本点,非静态成员类的一种常见用法是,允许外围类的实例被看做是另外一个不相关的类的实例。例如,Map接口的实现使用了大量的非静态成员类来实现它的一些视图,例如keySet,valueSet等。

举例说明用法:

​ 最近遇到这样一个需求,需要求2个特定日志之间所有的日期的年份和日。显然,从OOP的角度来看,我们需要把年份和日组合成一个对象,然后返回这个对象的一个集合。而这个对象所属的类,在其他地方是不需要使用到的,因此没有必要上升到一个顶层的外部类,故使用静态内部类最合适。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static class YearAndDay{
private int year;
private int day;

YearAndDay(int year,int day){
this.year = year;
this.day = day;
}
public void getYear(){
return this.year;
}
public void getDay(){
return this.day;
}
}

匿名内部类和局部类

​ 这两种类不细说,以后再补充。

关于异常处理

异常处理需要考虑的三个问题

处理程序异常,需要解决以下3个问题:

  1. Where:哪个地方发生了异常?
  2. Who: 谁来处理异常?
  3. How: 如何处理异常?

平时自己在写代码的时候,经常会纠结,我这个代码发生了异常,这个异常是需要向外抛出给调用者,还是要自己处理?《码出高效》给出这样一个基本原则:

如果异常在当前方法的处理能力范围之内且没有必要对外透出,那么就直接捕获异常并做相应的处理;否则就向上抛出,由上层方法或者框架来处理。

FileNotFound为例子。读取一个文件,如果文件不存在,这个异常是需要对外抛出还是自己处理呢?其他方法跟该方法交互的时候,一般会期待该方法:1. 根据文件内容进行一些设置(例如设置环境变量,实例化一些config等)2. 返回内容。当文件读取这个阶段出错的时候,我们是需要及时对调用方返回状态信息的。如果什么都不做,可能就会导致调用方误认为被调用方工作正常,从而导致调用方后续依赖调用方本次工作结果的工作发生异常。此时,及时反馈就很重要了。所以个人认为,在读文件这个操作对一些环境设置负责的时候,如果发生异常,需要向上抛出。具体需要依赖场景。以下是我认为的一些典型场景:

  • 读取文件内容负责对外返回一定的处理信息。调用方和被调用方之间如果具有一定的契约(例如,约定返回Null就是出现异常),那么此时调用方可以对异常进行处理而不对外抛出。如果没有明确的约定,那么最佳实践就是抛出异常,在语法层面要求被调用方对异常进行处理。

关于空指针异常

书中提到了2种编程理念:1. 契约式编程 2. 防御式编程

推荐拥抱防御式编程的理念(感觉跟我司的“人性本恶”有一拼^_^),即不信任任何一方的输入和输出。当然这是相对的,否则又会陷入无限判空的地狱中。