Docker 从 2013 年发展到现在,它的普及率已经可以和最常用的 MySQL 和 Redis 并驾齐驱了,从最初偶尔出现在面试中,到现在几乎成为面试中必问的问题之一。如果再不了解 Docker 相关的知识点,可能就会与自己心仪的职位擦肩而过。所以本课时将会带领你对 Docker 相关的知识做一个全面的认识。
我们本课时的面试题是,Docker 是什么?它有什么优点?
#### 典型回答
Docker 是一个开源(开放源代码)的应用容器引擎,可以方便地对容器进行管理。可通过 Docker 打包各种环境应用配置,比如安装 JDK 环境、发布自己的 Java 程序等,然后再把它发布到任意 Linux 机器上。
Docker 中有三个重要的概念,具体如下。
* 镜像(Image):一个特殊的文件操作系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的配置参数(如匿名卷、环境变量、用户等), 镜像不包含任何动态数据,其内容在构建之后也不会被改变。
* 容器(Container):它是用来运行镜像的。例如,我们拉取了一个 MySQL 镜像之后,只有通过创建并启动 MySQL 容器才能正常的运行 MySQL,容器可以进行创建、启动、停止、删除、暂停等操作。
* 仓库(Repository):用来存放镜像文件的地方,我们可以把自己制作的镜像上传到仓库中,Docker 官方维护了一个公共仓库 Docker Hub,你也可以点击这里查询并下载所有的公共镜像。
![](https://img.kancloud.cn/68/6d/686d25be1853473ad3e2d88f05d1b8f7_1822x958.png)
在 Docker 出现之前,我们如果要发布自己的 Java 程序,就需要在服务器上安装 JDK(或者 JRE)、Tomcat 容器,然后配置 Tomcat 参数,对 JVM 参数进行调优等操作。然而如果要在多台服务器上运行 Java 程序,则需要将同样繁杂的步骤在每台服务器都重复执行一遍,这样显然比较耗时且笨拙的。
后来有了虚拟机的技术,我们就可以将配置环境打包到一个虚拟机镜像中,然后在需要的服务器上装载这些虚拟机,从而实现了运行环境的复制,但虚拟机会占用很多的系统资源,比如内存资源和硬盘资源等,并且虚拟机的运行需要加载整个操作系统,这样就会浪费掉好几百兆的内存资源,最重要的是因为它需要加载整个操作系统所以它的运行速度就很慢,并且还包含了一些我们用不到的冗余功能。
因为虚拟机的这些缺点,所以在后来就有了 Linux 容器(Linux Containers,LXC),它是一种进程级别的 Linux 容器,用它可以模拟一个完整的操作系统。相比于虚拟机来说,Linux 容器所占用的系统资源更少,启动速度也更快,因为它本质上是一个进程而非真实的操作系统,因此它的启动速度就比较快。
而 Docker 则是对 Linux 容器的一种封装,并提供了更加方便地使用接口,所以 Docker 一经推出就迅速流行起来。Docker 和虚拟机(VM)区别如下图所示:
![](https://img.kancloud.cn/5f/78/5f783a35eafac24465e7206fc30f296f_1290x530.png)
Docker 具备以下 6 个优点。
* 轻量级:Docker 容器主要利用并共享主机内核,它并不是完整的操作系统,因此它更加轻量化。
* 灵活:它可以将复杂的应用程序容器化,因此它非常灵活和方便。
* 可移植:可以在本地构建 Docker 容器,并把它部署到云服务器或任何地方进行使用。
* 相互隔离,方便升级:容器是高度自给自足并相互隔离的容器,这样就可以在不影响其他容器的情况下更换或升级你的 Docker 容器了。
* 可扩展:可以在数据中心内增加并自动分发容器副本。
* 安全:Docker 容器可以很好地约束和隔离应用程序,并且无须用户做任何配置。
#### 考点分析
通过此面试题可以考察出面试者是否真的使用或了解过 Docker 技术,然而对于面试官来说,最关注的是面试者是否了解 Docker 和 Java 程序结合时会出现的一些问题,因此这部分的内容需要读者特别留意一下。
和此知识点相关的面试题还有以下这些:
* Docker 的常用命令有哪些?
* 在 Docker 中运行 Java 程序可能会存在什么问题?
#### 知识扩展
* [ ] Docker 常用命令
我们在安装了 Docker Disktop(客户端)就可以用 docker --version 命令来查看 Docker 的版本号,使用示例如下:
```
$ docker --version
Docker version 19.03.8, build afacb8b
```
然后可以到 Docker Hub 上查找我们需要的镜像,比如 Redis 镜像,如下图所示:
![](https://img.kancloud.cn/7b/f5/7bf5ff759ed77973aa9fba2414c5b32b_2622x1918.png)
接着我们选择并点击最多人下载的镜像,如下图所示:
![](https://img.kancloud.cn/10/ad/10adf055d2510123a4feb9d07b406075_2582x1802.png)
从描述中找到我们需要装载 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 中可能会存在意外停止和线程创建过多的问题以及解决方案。
通过以上内容的学习相信你对 Docker 已经有了一个系统的认识,需要特别注意是 Docker 在 Java 老版本中可能出现的问题以及解决方案,这一点在面试中经常会被问到。
OK,这一课时就讲到这里啦,恭喜你已经学习完了关于本系列的所有课程。如果你觉得课程不错,从中有所收获的话,不要忘了推荐给身边的朋友哦,最后希望大家都有所提高、不断成长,谢谢~
- 前言
- 开篇词
- 开篇词:大厂技术面试“潜规则”
- 模块一:Java 基础
- 第01讲:String 的特点是什么?它有哪些重要的方法?
- 第02讲:HashMap 底层实现原理是什么?JDK8 做了哪些优化?
- 第03讲:线程的状态有哪些?它是如何工作的?
- 第04讲:详解 ThreadPoolExecutor 的参数含义及源码执行流程?
- 第05讲:synchronized 和 ReentrantLock 的实现原理是什么?它们有什么区别?
- 第06讲:谈谈你对锁的理解?如何手动模拟一个死锁?
- 第07讲:深克隆和浅克隆有什么区别?它的实现方式有哪些?
- 第08讲:动态代理是如何实现的?JDK Proxy 和 CGLib 有什么区别?
- 第09讲:如何实现本地缓存和分布式缓存?
- 第10讲:如何手写一个消息队列和延迟消息队列?
- 模块二:热门框架
- 第11讲:底层源码分析 Spring 的核心功能和执行流程?(上)
- 第12讲:底层源码分析 Spring 的核心功能和执行流程?(下)
- 第13讲:MyBatis 使用了哪些设计模式?在源码中是如何体现的?
- 第14讲:SpringBoot 有哪些优点?它和 Spring 有什么区别?
- 第15讲:MQ 有什么作用?你都用过哪些 MQ 中间件?
- 模块三:数据库相关
- 第16讲:MySQL 的运行机制是什么?它有哪些引擎?
- 第17讲:MySQL 的优化方案有哪些?
- 第18讲:关系型数据和文档型数据库有什么区别?
- 第19讲:Redis 的过期策略和内存淘汰机制有什么区别?
- 第20讲:Redis 怎样实现的分布式锁?
- 第21讲:Redis 中如何实现的消息队列?实现的方式有几种?
- 第22讲:Redis 是如何实现高可用的?
- 模块四:Java 进阶
- 第23讲:说一下 JVM 的内存布局和运行原理?
- 第24讲:垃圾回收算法有哪些?
- 第25讲:你用过哪些垃圾回收器?它们有什么区别?
- 第26讲:生产环境如何排除和优化 JVM?
- 第27讲:单例的实现方式有几种?它们有什么优缺点?
- 第28讲:你知道哪些设计模式?分别对应的应用场景有哪些?
- 第29讲:红黑树和平衡二叉树有什么区别?
- 第30讲:你知道哪些算法?讲一下它的内部实现过程?
- 模块五:加分项
- 第31讲:如何保证接口的幂等性?常见的实现方案有哪些?
- 第32讲:TCP 为什么需要三次握手?
- 第33讲:Nginx 的负载均衡模式有哪些?它的实现原理是什么?
- 第34讲:Docker 有什么优点?使用时需要注意什么问题?
- 彩蛋
- 彩蛋:如何提高面试成功率?