Docker 從 2013 年發展到現在,它的普及率已經可以和最常用的 MySQL 和 Redis 并駕齊驅了,從最初偶爾出現在面試中,到現在幾乎成為面試中必問的問題之一。如果再不了解 Docker 相關的知識點,可能就會與自己心儀的職位擦肩而過。所以本課時將會帶領你對 Docker 相關的知識做一個全面的認識。
我們本課時的面試題是,Docker 是什么?它有什么優點?
#### 典型回答
Docker 是一個開源(開放源代碼)的應用容器引擎,可以方便地對容器進行管理。可通過 Docker 打包各種環境應用配置,比如安裝 JDK 環境、發布自己的 Java 程序等,然后再把它發布到任意 Linux 機器上。
Docker 中有三個重要的概念,具體如下。
* 鏡像(Image):一個特殊的文件操作系統,除了提供容器運行時所需的程序、庫、資源、配置等文件外,還包含了一些為運行時準備的配置參數(如匿名卷、環境變量、用戶等), 鏡像不包含任何動態數據,其內容在構建之后也不會被改變。
* 容器(Container):它是用來運行鏡像的。例如,我們拉取了一個 MySQL 鏡像之后,只有通過創建并啟動 MySQL 容器才能正常的運行 MySQL,容器可以進行創建、啟動、停止、刪除、暫停等操作。
* 倉庫(Repository):用來存放鏡像文件的地方,我們可以把自己制作的鏡像上傳到倉庫中,Docker 官方維護了一個公共倉庫 Docker Hub,你也可以點擊這里查詢并下載所有的公共鏡像。

在 Docker 出現之前,我們如果要發布自己的 Java 程序,就需要在服務器上安裝 JDK(或者 JRE)、Tomcat 容器,然后配置 Tomcat 參數,對 JVM 參數進行調優等操作。然而如果要在多臺服務器上運行 Java 程序,則需要將同樣繁雜的步驟在每臺服務器都重復執行一遍,這樣顯然比較耗時且笨拙的。
后來有了虛擬機的技術,我們就可以將配置環境打包到一個虛擬機鏡像中,然后在需要的服務器上裝載這些虛擬機,從而實現了運行環境的復制,但虛擬機會占用很多的系統資源,比如內存資源和硬盤資源等,并且虛擬機的運行需要加載整個操作系統,這樣就會浪費掉好幾百兆的內存資源,最重要的是因為它需要加載整個操作系統所以它的運行速度就很慢,并且還包含了一些我們用不到的冗余功能。
因為虛擬機的這些缺點,所以在后來就有了 Linux 容器(Linux Containers,LXC),它是一種進程級別的 Linux 容器,用它可以模擬一個完整的操作系統。相比于虛擬機來說,Linux 容器所占用的系統資源更少,啟動速度也更快,因為它本質上是一個進程而非真實的操作系統,因此它的啟動速度就比較快。
而 Docker 則是對 Linux 容器的一種封裝,并提供了更加方便地使用接口,所以 Docker 一經推出就迅速流行起來。Docker 和虛擬機(VM)區別如下圖所示:

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 鏡像,如下圖所示:

接著我們選擇并點擊最多人下載的鏡像,如下圖所示:

從描述中找到我們需要裝載 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 有什么優點?使用時需要注意什么問題?
- 彩蛋
- 彩蛋:如何提高面試成功率?