背景知识

Docker 从 2013 年发展到现在,它的普及率现已能够和最常用的 MySQL 和 Redis 齐头并进了。

Docker 是一个开源(开放源代码)的运用容器引擎,能够方便地对容器进行管理。可通过 Docker 打包各种环境运用装备,比方装置 JDK 环境、发布自己的 Java 程序等,然后再把它发布到恣意 Linux 机器上。

Docker 中有三个重要的概念,具体如下。

  • 镜像(Image):一个特别的文件操作系统,除了供给容器运转时所需的程序、库、资源、装备等文件外,还包含了一些为运转时预备的装备参数(如匿名卷、环境变量、用户等),镜像不包含任何动态数据,其内容在构建之后也不会被改动。
  • 容器(Container):它是用来运转镜像的。例如,咱们拉取了一个 MySQL 镜像之后,只要通过创立并发动 MySQL 容器才能正常的运转 MySQL,容器能够进行创立、发动、中止、删去、暂停等操作。
  • 库房(Repository):用来存放镜像文件的地方,咱们能够把自己制作的镜像上传到库房中,Docker 官方保护了一个公共库房 Docker Hub。

Docker 有什么优点?

在 Docker 呈现之前,咱们假如要发布自己的 Java 程序,就需求在服务器上装置 JDK(或许 JRE)、Tomcat 容器,然后装备 Tomcat 参数,对 JVM 参数进行调优等操作。但是假如要在多台服务器上运转 Java 程序,则需求将同样繁杂的步骤在每台服务器都重复履行一遍,这样明显比较耗时且蠢笨的。

后来有了虚拟机的技术,咱们就能够将装备环境打包到一个虚拟机镜像中,然后在需求的服务器上装载这些虚拟机,然后实现了运转环境的复制,但虚拟机会占用很多的系统资源,比方内存资源和硬盘资源等,而且虚拟机的运转需求加载整个操作系统,这样就会糟蹋掉好几百兆的内存资源,最重要的是由于它需求加载整个操作系统所以它的运转速度就很慢,而且还包含了一些咱们用不到的冗余功能。

由于虚拟机的这些缺点,所以在后来就有了 Linux 容器(Linux Containers,LXC),它是一种进程等级的 Linux 容器,用它能够模仿一个完好的操作系统。比较于虚拟机来说,Linux 容器所占用的系统资源更少,发动速度也更快,由于它本质上是一个进程而非真实的操作系统,因而它的发动速度就比较快。

而 Docker 则是对 Linux 容器的一种封装,并供给了更加方便地运用接口,所以 Docker 一经推出就敏捷流行起来。Docker 和虚拟机(VM)差异如下图所示:

Docker 有什么优点?
Docker 具备以下 6 个优点。

  • 轻量级:Docker 容器主要运用并共享主机内核,它并不是完好的操作系统,因而它更加轻量化。
  • 灵敏:它能够将复杂的运用程序容器化,因而它非常灵敏和方便。
  • 可移植:能够在本地构建 Docker 容器,并把它布置到云服务器或任何地方进行运用。
  • 彼此阻隔,方便晋级:容器是高度自给自足并彼此阻隔的容器,这样就能够在不影响其他容器的情况下更换或晋级你的 Docker 容器了。
  • 可扩展:能够在数据中心内添加并主动分发容器副本。
  • 安全:Docker 容器能够很好地约束和阻隔运用程序,而且无须用户做任何装备。

重点剖析

  • Docker 的常用指令有哪些?
  • 在 Docker 中运转 Java 程序可能会存在什么问题?

知识扩展

Docker 常用指令

咱们在装置了 Docker Disktop(客户端)就能够用 docker –version 指令来检查 Docker 的版别号,运用示例如下:

$ docker --version
Docker version 19.03.8, build afacb8b

然后能够到 Docker Hub 上查找咱们需求的镜像,比方 Redis 镜像,如下图所示:

Docker 有什么优点?

接着咱们选择并点击最多人下载的镜像,如下图所示:

Docker 有什么优点?
从描述中找到咱们需求装载 Redis 的版别,然后运用 docker pull redis@ 版别号来拉取相关的镜像,或许运用 docker pull redis 直接拉取最新(版别)的 Redis 镜像,如下所示:

$ docker pull redis
Using default tag: latest
latest: Pulling from library/redis
Digest: sha256:800f2587bf3376cb01e6307afe599ddce9439deafbd4fb8562829da96085c9c5
Status: Image is up to date for redis:latest
docker.io/library/redis:latest

紧接着就能够运用 docker images 指令来检查所有下载的镜像,如下所示:

$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
redis latest 235592615444 13 days ago 104MB

有了镜像之后咱们就能够运用 docker run 来创立并运转容器了,运用指令如下:

$ docker run --name myredis -d redis
22f560251e68b5afb5b7b52e202dcb3d47327f2136700d5a17bca7e37fc486bf

检查运转中的容器,指令如下:

¥ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
22f560251e68 redis "docker-entrypoint.s…" About a minute ago Up About a minute 6379/tcp myredis

其间,“myredis”为容器的称号,“6379/tcp”为 Redis 的端口号,容器的 ID 为“22f560251e68”。 最终咱们运用如下指令来连接 Redis:

$ docker  exec -it myredis  redis-cli 127.0.0.1:6379>

其他常用指令如下:

  • 容器中止:docker stop 容器称号

  • 发动容器:docker start 容器称号

  • 删去容器:docker rm 容器称号

  • 删去镜像:docker rmi 镜像称号

  • 检查运转的所有容器:docker ps

  • 检查所有容器:docker ps -a

  • 容器复制文件到物理机:docker cp 容器称号:容器目录 物理机目录

  • 物理机复制文件到容器:docker cp 物理机目录 容器称号:容器目录

Docker 可能存在的问题

Java 相对于 Docker 来说明显具有更悠长的历史,因而在前期的 Java 版别中(JDK 8u131)由于不能很好地辨认 Docker 相关的装备信息,然后导致可能会呈现 Java 程序意外被中止的情况或许是过度创立线程数而导致并发性能下降的问题。

Java 程序意外中止的主要原因是由于,在 Docker 中运转的 Java 程序由于没有明确指定 JVM 堆和直接内存等参数,而 Java 程序也不能很好地辨认 Docker 的相关容量装备,导致 Java 程序试图获取了超越 Docker 本身的容量,而被 Docker 容器强制完毕进程的情况(这是 Docker 本身的防御保护机制)。

过度创立线程是由于前期的 Java 版别并不能很好地辨认 Docker 容器的 CPU 资源,因而会错误地辨认和创立过多的线程数。比方 ParallelStreams 和 ForkJoinPool 等,它们默认便是根据当时系统的 CPU 中心数来创立对应的线程数的,但由于在 Docker 中的 Java 程序并不能很好地辨认 CPU 中心数,就会导致创立的线程数量大于 CPU 的中心数量,然后导致并发功率下降的情况。

ParallelStreams 的根本用法如下:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
numbers.parallelStream().forEach(count -> {
    System.out.println("val:" + count);
});

ParallelStreams 是将任务提交给 ForkJoinPool 来实现的,ForkJoinPool 获取本地 CPU 中心数的源码如下:

private static ForkJoinPool makeCommonPool() {
    // 疏忽其他代码
    if (parallelism < 0 && // default 1 less than #cores
        (parallelism = Runtime.getRuntime().availableProcessors() - 1) <= 0)
        parallelism = 1;
    if (parallelism > MAX_CAP)
        parallelism = MAX_CAP;
    return new ForkJoinPool(parallelism, factory, handler, LIFO_QUEUE,
                            "ForkJoinPool.commonPool-worker-");
}

其间,“Runtime.getRuntime().availableProcessors()”是用来获取本地线程数量的。

要处理以上这些问题的办法,最简单的处理方案便是晋级 Java 版别,比方 Java 10 就能够很好地辨认 Docker 容器的这些约束。但假如运用的是老版别的 Java,那么需求在发动 JVM 的时候合理的装备堆、元数据区等内存区域大小,并指定 ForkJoinPool 的最大线程数,如下所示:

-Djava.util.concurrent.ForkJoinPool.common.parallelism=8

总结

本文介绍了 Docker 的概念以及 Docker 中最重要的三个组件:镜像、容器和库房,而且介绍了 Docker 的 6 大特色:轻量级、灵敏、可移植、彼此阻隔、可扩展和安全等特色;同时还介绍了 Docker 的常见运用指令;最终介绍了 Docker 可能在老版别(JDK 8u131 之前)的 Java 中可能会存在意外中止和线程创立过多的问题以及处理方案。