Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

在Java8的stream之前,将对象进行排序的时候,可能需要对象实现Comparable接口,或者自己实现一个Comparator,

比如这样子

我的对象是Entity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Entity {

private Long id;

private Long sortValue;

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public Long getSortValue() {
return sortValue;
}

public void setSortValue(Long sortValue) {
this.sortValue = sortValue;
}
}

Comparator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyComparator implements Comparator {
@Override
public int compare(Object o1, Object o2) {
Entity e1 = (Entity) o1;
Entity e2 = (Entity) o2;
if (e1.getSortValue() < e2.getSortValue()) {
return -1;
} else if (e1.getSortValue().equals(e2.getSortValue())) {
return 0;
} else {
return 1;
}
}
}

比较代码

1
2
3
4
5
6
7
8
9
10
11
12
13
private static MyComparator myComparator = new MyComparator();

public static void main(String[] args) {
List<Entity> list = new ArrayList<Entity>();
Entity e1 = new Entity();
e1.setId(1L);
e1.setSortValue(1L);
list.add(e1);
Entity e2 = new Entity();
e2.setId(2L);
e2.setSortValue(null);
list.add(e2);
Collections.sort(list, myComparator);

看到这里的e2的排序值是null,在Comparator中如果要正常运行的话,就得判空之类的,这里有两点需要,一个是不想写这个MyComparator,然后也没那么好排除掉list里排序值,那么有什么办法能解决这种问题呢,应该说java的这方面真的是很强大

看一下nullsFirst的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
final static class NullComparator<T> implements Comparator<T>, Serializable {
private static final long serialVersionUID = -7569533591570686392L;
private final boolean nullFirst;
// if null, non-null Ts are considered equal
private final Comparator<T> real;

@SuppressWarnings("unchecked")
NullComparator(boolean nullFirst, Comparator<? super T> real) {
this.nullFirst = nullFirst;
this.real = (Comparator<T>) real;
}

@Override
public int compare(T a, T b) {
if (a == null) {
return (b == null) ? 0 : (nullFirst ? -1 : 1);
} else if (b == null) {
return nullFirst ? 1: -1;
} else {
return (real == null) ? 0 : real.compare(a, b);
}
}

核心代码就是下面这段,其实就是帮我们把前面要做的事情做掉了,是不是挺方便的,小记一下哈

echo 实操技巧

最近做 docker 系列,会经常需要进到 docker 内部,如上一篇介绍的,这些镜像一般都有用 ubuntu 或者alpine 这样的 Linux 系统作为底包,如果构建镜像的时候没有替换源的话,因为特殊的网络原因,在内部想编辑下东西要安装个类似于 vim 这样的编辑器就会很慢很慢,像视频里 two thousand years later~ 而且如果在容器内部想改源配置的话也要编辑器,就陷入了一个鸡生蛋,跟蛋生鸡的死锁问题中,对于 linux 大神来说应该有一万种方法解决这个问题,对于我这个渣渣来说可能只想到了这个土方法,先 cp backup 一下 sources.list, 再 echo “xxx” > sources.list, 这里就碰到了一个问题,这个 sources.list 一般不止一行,直接 echo 的话就解析不了了,不过 echo 可以支持”\n”转义,就是加-e看一下解释和示例,我这里使用了 tldr ,可以用 npm install -g tldr 安装,也可以直接用man, 或者–help 来查看使用方式

查看镜像底包

还有一点也是在这个时候要安装 vim 之类的,得知道是什么镜像底包,如果是用 uname 指令,其实看到的是宿主机的系统,得用cat /etc/issue


这里稍稍记一下

寻找系统镜像源

目前国内系统源用得比较多的是阿里云源,不过这里也推荐清华源, 中科大源, 浙大源 这里不要脸的推荐下母校的源,不过还不是很完善,尽情期待下。

运行第一个 Dockerfile

上一篇的 Dockerfile 我们停留在构建阶段,现在来把它跑起来

1
2
docker run -d -p 80 --name static_web nicksxs/static_web \
nginx -g "daemon off;"

这里的-d表示以分离模型运行docker (detached),然后-p 是表示将容器的 80 端口开放给宿主机,然后容器名就叫 static_web,使用了我们上次构建的 static_web 镜像,后面的是让 nginx 在前台运行

可以看到返回了个容器 id,但是具体情况没出现,也没连上去,那我们想看看怎么访问在 Dockerfile 里写的静态页面,我们来看下docker 进程

发现为我们随机分配了一个宿主机的端口,32768,去服务器的防火墙把这个外网端口开一下,看看是不是符合我们的预期呢

好像不太对额,应该是 ubuntu 安装的 nginx 的默认工作目录不对,我们来进容器看看,再熟悉下命令docker exec -it 4792455ca2ed /bin/bash
记得容器 id 换成自己的,进入容器后得找找 nginx 的配置文件,通常在/etc/nginx,/usr/local/etc等目录下,然后找到我们的目录是在这

所以把刚才的内容复制过去再试试

目标达成,give me five✌️

第二个 Dockerfile

然后就想来动态一点的,毕竟写过 PHP,就来试试 PHP
再建一个目录叫 dynamic_web,里面创建 src 目录,放一个 index.php
内容是

1
2
<?php
echo "Hello World!";

然后在 dynamic_web 目录下创建 Dockerfile,

1
2
3
FROM trafex/alpine-nginx-php7:latest
COPY src/ /var/www/html
EXPOSE 80

Dockerfile 虽然只有三行,不过要着重说明下,这个底包其实不是docker 官方的,有两点考虑,一点是官方的基本都是 php apache 的镜像,还有就是 alpine这个,截取一段中文介绍

Alpine 操作系统是一个面向安全的轻型 Linux 发行版。它不同于通常 Linux 发行版,Alpine 采用了 musl libc 和 busybox 以减小系统的体积和运行时资源消耗,但功能上比 busybox 又完善的多,因此得到开源社区越来越多的青睐。在保持瘦身的同时,Alpine 还提供了自己的包管理工具 apk,可以通过 https://pkgs.alpinelinux.org/packages 网站上查询包信息,也可以直接通过 apk 命令直接查询和安装各种软件。
Alpine 由非商业组织维护的,支持广泛场景的 Linux发行版,它特别为资深/重度Linux用户而优化,关注安全,性能和资源效能。Alpine 镜像可以适用于更多常用场景,并且是一个优秀的可以适用于生产的基础系统/环境。

Alpine Docker 镜像也继承了 Alpine Linux 发行版的这些优势。相比于其他 Docker 镜像,它的容量非常小,仅仅只有 5 MB 左右(对比 Ubuntu 系列镜像接近 200 MB),且拥有非常友好的包管理机制。官方镜像来自 docker-alpine 项目。

目前 Docker 官方已开始推荐使用 Alpine 替代之前的 Ubuntu 做为基础镜像环境。这样会带来多个好处。包括镜像下载速度加快,镜像安全性提高,主机之间的切换更方便,占用更少磁盘空间等。

一方面在没有镜像的情况下,拉取 docker 镜像还是比较费力的,第二个就是也能节省硬盘空间,所以目前有大部分的 docker 镜像都将 alpine 作为基础镜像了
然后再来构建下

这里还有个点,就是上面的那个镜像我们也是 EXPOSE 80端口,然后外部宿主机会随机映射一个端口,为了偷个懒,我们就直接指定外部端口了
docker run -d -p 80:80 dynamic_web打开浏览器发现访问不了,咋回事呢
因为我们没看trafex/alpine-nginx-php7:latest这个镜像说明,它内部的服务是 8080 端口的,所以我们映射的暴露端口应该是 8080,再用docker run -d -p 80:8080 dynamic_web这个启动,

限制下 docker 的 cpu 使用率

这里我们开始玩一点有意思的,我们在容器里装下 vim 和 gcc,然后写这样一段 c 代码

1
2
3
4
5
6
7
#include <stdio.h>
int main(void)
{
int i = 0;
for(;;) i++;
return 0;
}

就是一个最简单的死循环,然后在容器里跑起来

1
2
$ gcc 1.c 
$ ./a.out

然后我们来看下系统资源占用(CPU)
Xs562iawhHyMxeO
上图是在容器里的,可以看到 cpu 已经 100%了
然后看看容器外面的
ecqH8XJ4k7rKhzu
可以看到一个核的 cpu 也被占满了,因为是个双核的机器,并且代码是单线程的
然后呢我们要做点啥
因为已经在这个 ubuntu 容器中装了 vim 和 gcc,考虑到国内的网络,所以我们先把这个容器 commit 一下,

1
docker commit -a "nick" -m "my ubuntu" f63c5607df06 my_ubuntu:v1

然后再运行起来

1
docker run -it --cpus=0.1 my_ubuntu:v1 bash


我们的代码跟可执行文件都还在,要的就是这个效果,然后再运行一下

结果是这个样子的,有点神奇是不,关键就在于 run 的时候的--cpus=0.1这个参数,它其实就是基于我前一篇说的 cgroup 技术,能将进程之间的cpu,内存等资源进行隔离

开始第一个 Dockerfile

上一面为了复用那个我装了 vim 跟 gcc 的容器,我把它提交到了本地,使用了docker commit命令,有点类似于 git 的 commit,但是这个不是个很好的操作方式,需要手动介入,这里更推荐使用 Dockerfile 来构建镜像

1
2
3
4
5
6
7
8
From ubuntu:latest
MAINTAINER Nicksxs "nicksxs@hotmail.com"
RUN sed -i s@/archive.ubuntu.com/@/mirrors.aliyun.com/@g /etc/apt/sources.list
RUN apt-get clean
RUN apt-get update && apt install -y nginx
RUN echo 'Hi, i am in container' \
> /usr/share/nginx/html/index.html
EXPOSE 80

先解释下这是在干嘛,首先是这个From ubuntu:latest基于的 ubuntu 的最新版本的镜像,然后第二行是维护人的信息,第三四行么作为墙内人你懂的,把 ubuntu 的源换成阿里云的,不然就有的等了,然后就是装下 nginx,往默认的 nginx 的入口 html 文件里输入一行欢迎语,然后暴露 80 端口
然后我们使用sudo docker build -t="nicksxs/static_web" .命令来基于这个 Dockerfile 构建我们自己的镜像,过程中是这样的


可以看到图中,我的 Dockerfile 是 7 行,里面就执行了 7 步,并且每一步都有一个类似于容器 id 的层 id 出来,这里就是一个比较重要的东西,docker 在构建的时候其实是有这个层的概念,Dockerfile 里的每一行都会往上加一层,这里有还注意下命令后面的.,代表当前目录下会自行去寻找 Dockerfile 进行构建,构建完了之后我们再看下我们的本地镜像

我们自己的镜像出现啦
然后有个问题,如果这个构建中途报了错咋办呢,来试试看,我们把 nginx 改成随便的一个错误名,nginxx(不知道会不会运气好真的有这玩意),再来 build 一把

找不到 nginxx 包,是不是这个镜像就完全不能用呢,当然也不是,因为前面说到了,docker 是基于层去构建的,可以看到前面的 4 个 step 都没报错,那我们基于最后的成功步骤创建下容器看看
也就是sudo docker run -t -i bd26f991b6c8 /bin/bash
答案是可以的,只是没装成功 nginx

还有一点注意到没,前面的几个 step 都有一句 Using cache,说明 docker 在构建镜像的时候是有缓存的,这也更能说明 docker 是基于层去构建镜像,同样的底包,同样的步骤,这些层是可以被复用的,这就是 docker 的构建缓存,当然我们也可以在 build 的时候加上--no-cache去把构建缓存禁用掉。

因为最近想搭一个phabricator用来做看板和任务管理,一开始了解这个是Easy大大有在微博推荐过,后来苏洋也在群里和博客里说到了,看上去还不错的样子,因为主角是docker所以就不介绍太多,后面有机会写一下。

docker最开始是之前在某位大佬的博客看到的,看上去有点神奇,感觉是一种轻量级的虚拟机,但是能做的事情好像差不多,那时候是在Ubuntu系统的vps里起一个Ubuntu的docker,然后在里面装个nginx,配置端口映射就可以访问了,后来也草草写过一篇使用docker搭建mysql集群,但是最近看了下好像是因为装docker的大佬做了一些别名还是什么操作,导致里面用的操作都不具有普遍性,而且主要是把搭的过程写了下,属于囫囵吞枣,没理解docker是干啥的,为啥用docker,就是操作了下,这几天借着搭phabricator的过程,把一些原来不理解,或者原来理解错误的地方重新理一下。

之前写的 mysql 集群,一主二备,这种架构在很多小型应用里都是这么配置的,而且一般是直接在三台 vps 里启动三个 mysql 实例,但是如果换成 docker 会有什么好处呢,其实就是方便部署,比如其中一台备库挂了,我要加一台,或者说备库的 qps 太高了,需要再加一个,如果要在 vps 上搭建的话,首先要买一台机器,等初始化,然后在上面修改源,更新,装 mysql ,然后配置主从,可能还要处理防火墙等等,如果把这些打包成一个 docker 镜像,并且放在自己的 docker registry,那就直接run 一下就可以了;还有比如在公司要给一个新同学整一套开发测试环境,以 Java 开发为例,要装 git,maven,jdk,配置 maven settings 和各种 rc,整合在一个镜像里的话,就会很方便了;再比如微服务的水平扩展。

但是为啥 docker 会有这种优势,听起来好像虚拟机也可以干这个事,但是虚拟机动辄上 G,而且需要 VMware,virtual box 等支持,不适合在Linux服务器环境使用,而且占用资源也会非常大。说得这么好,那么 docker 是啥呢

docker 主要使用 Linux 中已经存在的两种技术的一个整合升级,一个是 namespace,一个是cgroups,相比于虚拟机需要完整虚拟出一个操作系统运行基础,docker 基于宿主机内核,通过 namespace 和 cgroups 分隔进程,理念就是提供一个隔离的最小化运行依赖,这样子相对于虚拟机就有了巨大的便利性,具体的 namespace 和 cgroups 就先不展开讲,可以参考耗子叔的文章

安装

那么我们先安装下 docker,参考官方的教程,安装,我的系统是 ubuntu 的,就贴了 ubuntu 的链接,用其他系统的可以找到对应的系统文档安装,安装完了的话看看 docker 的信息

1
sudo docker info

输出以下信息

简单运行

然后再来运行个 hello world 呗,

1
sudo docker run hello-world

输出了这些

看看这个运行命令是怎么用的,一般都会看到这样子的,sudo docker run -it ubuntu bash, 前面的 docker run 反正就是运行一个容器的意思,-it是啥呢,还有这个什么 ubuntu bash,来看看docker run`的命令帮助信息

1
-i, --interactive                    Keep STDIN open even if not attached

就是要有输入,我们运行的时候能输入

1
-t, --tty                            Allocate a pseudo-TTY

要有个虚拟终端,

1
2
3
Usage:	docker run [OPTIONS] IMAGE [COMMAND] [ARG...]

Run a command in a new container

镜像

上面说的-it 就是这里的 options,后面那个 ubuntu 就是 image 辣,image 是啥呢

Docker 把应用程序及其依赖,打包在 image 文件里面,可以把它理解成为类似于虚拟机的镜像或者运行一个进程的代码,跑起来了的叫docker 容器或者进程,比如我们将要运行的docker run -it ubuntu bash的ubuntu 就是个 ubuntu 容器的镜像,将这个镜像运行起来后,我们可以进入容器像使用 ubuntu 一样使用它,来看下我们的镜像,使用sudo docker image ls就能列出我们宿主机上的 docker 镜像了

一个 ubuntu 镜像才 64MB,非常小巧,然后是后面的bash,我通过交互式启动了一个 ubuntu 容器,然后在这个启动的容器里运行了 bash 命令,这样就可以在容器里玩一下了

在容器里看下进程,

只有刚才运行容器的 bash 进程和我刚执行的 ps,这里有个可以注意下的,bash 这个进程的 pid 是 1,其实这里就用到了 linux 中的PID Namespace,容器会隔离出一个 pid 的名字空间,这里面的进程跟外部的 pid 命名独立

查看宿主机上的容器

1
sudo docker ps -a

如何进入一个正在运行中的 docker 容器

这个应该是比较常用的,因为比如是一个微服务容器,有时候就像看下运行状态,日志啥的

1
sudo docker exec -it [containerID] bash

查看日志

1
sudo docker logs [containerID]

我在运行容器的终端里胡乱输入点啥,然后通过上面的命令就可以看到啦

0%