Table of contents
1 Spring Boot 項目
1.1 Maven dependencies
1<parent>
2 <groupId>org.springframework.boot</groupId>
3 <artifactId>spring-boot-starter-parent</artifactId>
4 <version>3.2.4</version>
5</parent>
6
7<dependencies>
8 <dependency>
9 <groupId>org.springframework.boot</groupId>
10 <artifactId>spring-boot-starter-web</artifactId>
11 </dependency>
12 <dependency>
13 <groupId>org.springframework.boot</groupId>
14 <artifactId>spring-boot-starter-actuator</artifactId>
15 </dependency>
16</dependencies>
1.2 Main class
1@SpringBootApplication
2public class MainApplication {
3
4 public static void main(String[] args) {
5 SpringApplication.run(MainApplication.class, args);
6 }
7}
1.3 Application 配置
application.yml
:
1management:
2 endpoints:
3 web:
4 exposure:
5 include: health,info,env,beans,loggers
6
7# 等目前既 HTTP requests 完成曬之後先會 shutdown,等候時間上限 60 秒
8server:
9 shutdown: graceful
10spring:
11 lifecycle:
12 timeout-per-shutdown-phase: 60s
2 Dockerfile
關於 OpenJDK 用
jlink
command 保留有需要既 modules,可以睇返呢篇:
客製化 JRE。
2.1 只有 OpenJDK 既 Dockerfile
1FROM mcr.microsoft.com/openjdk/jdk:17-ubuntu AS jre-stage
2
3RUN jlink \
4 --add-modules \
5java.base,\
6java.compiler,\
7java.datatransfer,\
8java.desktop,\
9java.instrument,\
10java.logging,\
11java.management,\
12java.management.rmi,\
13java.naming,\
14java.net.http,\
15java.prefs,\
16java.rmi,\
17java.scripting,\
18java.se,\
19java.security.jgss,\
20java.security.sasl,\
21java.smartcardio,\
22java.sql,\
23java.sql.rowset,\
24java.transaction.xa,\
25java.xml,\
26java.xml.crypto,\
27jdk.accessibility,\
28jdk.charsets,\
29jdk.crypto.cryptoki,\
30jdk.crypto.ec,\
31jdk.dynalink,\
32jdk.internal.ed,\
33jdk.internal.le,\
34jdk.internal.vm.ci,\
35jdk.internal.vm.compiler,\
36jdk.internal.vm.compiler.management,\
37jdk.jdwp.agent,\
38jdk.jfr,\
39jdk.jsobject,\
40jdk.localedata,\
41jdk.management,\
42jdk.management.agent,\
43jdk.management.jfr,\
44jdk.naming.dns,\
45jdk.naming.rmi,\
46jdk.net,\
47jdk.nio.mapmode,\
48jdk.random,\
49jdk.sctp,\
50jdk.security.auth,\
51jdk.security.jgss,\
52jdk.unsupported,\
53jdk.xml.dom,\
54jdk.zipfs \
55 --strip-debug \
56 --no-man-pages \
57 --no-header-files \
58 --compress=2 \
59 --output /javaruntime
60
61
62
63FROM ubuntu:22.04
64
65# VOLUME /tmp
66EXPOSE 8080
67
68RUN mkdir -m 445 /app && mkdir -m 445 /app/jre
69WORKDIR /app
70
71RUN addgroup --system mygroup && adduser --system --shell /bin/false --ingroup mygroup myuser
72USER myuser
73
74ENV PATH "/app/jre/bin:$PATH"
75COPY --from=jre-stage --chown=root:root --chmod=445 /javaruntime /app/jre
76
77# CMD "java" "-jar" "app.jar"
78ENTRYPOINT ["java", "-jar", "app.jar"]
79
80COPY --chown=root:root --chmod=444 /target/app.jar app.jar
2.1.1 Build script
我地可以配合以下既 build script:
1CALL mvn clean package
2
3PUSHD target
4REN *.jar app.jar
5POPD
6
7docker image build -t spring-boot-3-demo -f Dockerfile-openjdk.txt .
註:將 Dockerfile 保存做 Dockerfile-openjdk.txt
。
2.2 行埋 Maven build 既 Dockerfile
1FROM maven:3.9.6-eclipse-temurin-17 AS mvn-stage
2RUN mkdir /project
3COPY . /project
4WORKDIR /project
5RUN mvn clean package && mv /project/target/*.jar /project/target/app.jar
6
7
8
9FROM mcr.microsoft.com/openjdk/jdk:17-ubuntu AS jre-stage
10
11RUN jlink \
12 --add-modules \
13java.base,\
14java.compiler,\
15java.datatransfer,\
16java.desktop,\
17java.instrument,\
18java.logging,\
19java.management,\
20java.management.rmi,\
21java.naming,\
22java.net.http,\
23java.prefs,\
24java.rmi,\
25java.scripting,\
26java.se,\
27java.security.jgss,\
28java.security.sasl,\
29java.smartcardio,\
30java.sql,\
31java.sql.rowset,\
32java.transaction.xa,\
33java.xml,\
34java.xml.crypto,\
35jdk.accessibility,\
36jdk.charsets,\
37jdk.crypto.cryptoki,\
38jdk.crypto.ec,\
39jdk.dynalink,\
40jdk.internal.ed,\
41jdk.internal.le,\
42jdk.internal.vm.ci,\
43jdk.internal.vm.compiler,\
44jdk.internal.vm.compiler.management,\
45jdk.jdwp.agent,\
46jdk.jfr,\
47jdk.jsobject,\
48jdk.localedata,\
49jdk.management,\
50jdk.management.agent,\
51jdk.management.jfr,\
52jdk.naming.dns,\
53jdk.naming.rmi,\
54jdk.net,\
55jdk.nio.mapmode,\
56jdk.random,\
57jdk.sctp,\
58jdk.security.auth,\
59jdk.security.jgss,\
60jdk.unsupported,\
61jdk.xml.dom,\
62jdk.zipfs \
63 --strip-debug \
64 --no-man-pages \
65 --no-header-files \
66 --compress=2 \
67 --output /javaruntime
68
69
70
71FROM ubuntu:22.04
72
73# VOLUME /tmp
74EXPOSE 8080
75
76RUN mkdir -m 445 /app && mkdir -m 445 /app/jre
77WORKDIR /app
78
79RUN addgroup --system mygroup && adduser --system --shell /bin/false --ingroup mygroup myuser
80USER myuser
81
82ENV PATH "/app/jre/bin:$PATH"
83COPY --from=jre-stage --chown=root:root --chmod=445 /javaruntime /app/jre
84
85# CMD "java" "-jar" "app.jar"
86ENTRYPOINT ["java", "-jar", "app.jar"]
87
88COPY --from=mvn-stage --chown=root:root --chmod=444 /project/target/app.jar app.jar
2.2.1 Build script
docker image build -t spring-boot-3-demo -f Dockerfile-maven-and-openjdk.txt .
註:將 Dockerfile 保存做 Dockerfile-maven-and-openjdk.txt
。
2.3 詳細解釋
- 我地需要用到 Docker 既 multi-stage build 功能黎完成一啲事前準備功夫。
- 包括:
- 用
jlink
客製化 OpenJDK,淨係保留對我地有用既 Java modules。
- 用
mvn
打包個 Spring Boot 項目既 JAR 檔。
- 呢啲事前準備功夫可以用 intermediate stages 黎達成,而呢啲 stages 所產生出黎既 containers 同最後既 Docker image 冇關係。
- 我地可以將 intermediate stages 裡面既 output 檔案抄去最終既 Docker image 度。
- 真正既 base image 係
ubuntu
。
VOLUME
instruction 係用黎定義我地個 Docker image 可能有啲需要保存既用戶數據。
- Docker Desktop 既 behavior 係會自動建立 anonymous volume(s),但如果我地用
docker container run
執行並且加左 --rm
,咁個 container 結束既時候,Docker 就會自動刪除相關既 anonymous volume(s)。
- 如果喺 cloud 既 K8s 上面運行,而
readOnlyFilesystem
設置左係 true
,而 containerd 又設置左 ignore_image_defined_volumes
係 true
,咁呢個 VOLUME
instruction 會被無視。
- 喺個 Docker container 裡面執行
mount
command 可以睇到 Dockerfile
既 VOLUME
instructions 有冇令 volume mounts 生效到。
EXPOSE
instruction 係用黎定義我地個 app 會行喺咩 port 上面。
RUN
instruction 係用黎執行任意既 Linux commands。
- 為左減少 layers 既數量,我地要盡量將啲 commands 放到一個
RUN
instruction 裡面,用 &&
黎串連多個 commands,咁 Docker 就只會為呢一個 RUN
instruction 增加一個 layer。
mkdir -m <permission>
係用黎創建目錄,同時改變埋目錄既權限。
WORKDIR
instruction 會改變 container 既 working directory,影響後續 RUN
、CMD
、ENTRYPOINT
、COPY
以及 ADD
instructions 既 working directory。
docker container exec
或者 kubectl exec
入去個 container 之後最初既 working directory 會係 WORKDIR
instruction 定義既 path。
- 用
pwd
command 可以顯示 working directory。
- 正因為咁,所以最後一句
COPY
instruction 會將 app.jar
抄到去個 ubuntu
base image 既 /app
(working directory)。
addgroup
、adduser
係用黎新增 group、新增 user。
- 加黎完全係為左安全需要。
- 因為 Docker 默認會用
root
user 黎做曬所有野,我地唔希望個 Docker container 用一個有好大權限既 user。
USER
instruction 會切換 user,影響後續既 RUN
instructions,亦會影響 runtime 既時候執行 CMD
或者 ENTRYPOINT
既 user。
- 基於安全考慮,user 唔應該用默認既
root
,否則就有好多權限。
ENV
instructions 係用黎設置 environment variable。
- 我地要重新設置
PATH
environment variable 係因為我地只係 base 左 ubuntu
image,再將 OpenJDK 既 binaries 抄入去。如果唔設置 PATH
既話,ubuntu
就唔知道可以喺 /app/jre/bin
度 lookup java
執行檔。
- 修改
PATH
比較正路既做法係將要加入既 path 放喺左邊,然後用 :
同現時既 paths 分隔。放喺左邊既原因係如果有兩個或以上既 paths 存在相同既執行檔,咁 Linux 會用最左邊果個,亦即係話擺喺左邊既呢個方法喺呢個情況下會比較有效。
COPY
instruction 可以幫我地抄檔案或者目錄。
--from
會幫我地由之前既 stage 既 file system 抄 /javaruntime
裡面啲野去 ubuntu
既 /app/jre
裡面。
- 如果冇
--from
,就會由本地既 working directory 抄入去。
- 可以用
--chown
順便做埋 chown
,將 owner、group 改做 <user>:<group>
。
- 可以用
--chmod
順便做埋 chmod
,修改權限。
- 例如將權限改做
445
,咁 owner、group 只可以 read,而 public(其他 users 或者 groups)只可以 read、execute。
#
係用黎寫 comment,咁果句就唔會生效。
- 我地可以用
CMD
或者 ENTRYPOINT
instruction 黎話畀 Docker 知 Docker container 執行既時候應該行咩 commands。
CMD
可以被覆蓋。
ENTRYPOINT
唔可以被覆蓋,一定會執行。
- 關於權限,我地既 OpenJDK、
app.jar
既 owner、group 都係 root
,但因為我地用左 USER
instruction 切換左做 myuser
,而呢個 user 屬於另一個叫 mygroup
既 group,所以佢地需要開放權限畀 public,咁 myuser
先可以用到佢地。
myuser
需要 OpenJDK 既 read、execute 權限。
myuser
需要 app.jar
既 read 權限,唔需要 execute 權限係因為佢唔係直接執行 app.jar
,而係透過 OpenJDK 裡面既 java
。
3 執行
3.1 Docker Desktop
docker container run --rm -p 8080:8080 spring-boot-3-demo
Read-only file system 模式:
docker container run --rm -p 8080:8080 --read-only spring-boot-3-demo
註:
- 用左
--read-only
既話,一定要令到 /tmp
有 volume mount,從而令 /tmp
可以寫入。
- 否則,如果 Spring Boot 既 embedded Tomcat 喺 runtime 既時候創建唔到目錄或者寫入唔到落 temp folder 既話,就會令成個 Spring Boot microservice 啟動失敗。
- 用 Docker Desktop 既話可以利用 Dockerfile 既
VOLUME /tmp
instruction。
3.2 Kubernetes
我地可以開啟 Docker Desktop 內置既 K8s 功能,或者用某個 cloud provider 既 K8s 服務。
3.2.1 單獨 Pod
雖然可以只係用 K8s Pod
,但一般都會用 K8s Deployment
配合 manual 或者 auto 既 horizontal scaling。
k8s-pod.yml
:
1kind: Pod
2apiVersion: v1
3metadata:
4 name: spring-boot-3-demo
5 namespace: default
6spec:
7 terminationGracePeriodSeconds: 60 # 唔可以少過 Spring Boot 既 graceful shutdown 時限配置
8 containers:
9 - name: spring-boot-3-demo
10 image: spring-boot-3-demo:latest
11 imagePullPolicy: Never # 只限本地測試用
12 securityContext:
13 readOnlyRootFilesystem: true # 安全需要
14 ports:
15 - containerPort: 8080
16 startupProbe:
17 httpGet:
18 path: /actuator/health/liveness
19 port: 8080
20 initialDelaySeconds: 5
21 periodSeconds: 5
22 failureThreshold: 30
23 timeoutSeconds: 5
24 livenessProbe:
25 httpGet:
26 path: /actuator/health/liveness
27 port: 8080
28 initialDelaySeconds: 5
29 periodSeconds: 10
30 failureThreshold: 3
31 timeoutSeconds: 5
32 readinessProbe:
33 httpGet:
34 path: /actuator/health/readiness
35 port: 8080
36 initialDelaySeconds: 5
37 periodSeconds: 10
38 failureThreshold: 3
39 timeoutSeconds: 5
40 resources:
41 limits:
42 cpu: 1000m
43 requests:
44 cpu: 100m
45 args: # 示範用 command line arguments 黎改變 Spring Boot behaviors
46 - --logging.level.root=DEBUG
47
48# 用 emptyDir 方法,令 /tmp 可以寫入
49 volumeMounts:
50 - name: empty-tmp-dir
51 mountPath: /tmp
52 volumes:
53 - name: empty-tmp-dir
54 emptyDir: {}
保存左個檔案之後執行:
kubectl apply -f k8s-pod.yml
之後可以 port-forward 個 K8s Pod
:
kubectl port-forward spring-boot-3-demo 8080
3.2.2 Deployment
用 K8s Deployment
既話就可以指定某個數量既 replicas,K8s 會幫我地管理啲 Pod
。
k8s-deployment.yml
:
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4 name: spring-boot-3-demo
5 namespace: default
6spec:
7 replicas: 2 # 呢個會係固定數量
8 selector:
9 matchLabels:
10 app: spring-boot-3-demo
11 template:
12 metadata:
13 labels:
14 app: spring-boot-3-demo
15 spec:
16 terminationGracePeriodSeconds: 60 # 唔可以少過 Spring Boot 既 graceful shutdown 時限配置
17 containers:
18 - name: spring-boot-3-demo
19 image: spring-boot-3-demo:latest
20 imagePullPolicy: Never # 只限本地測試用
21 securityContext:
22 readOnlyRootFilesystem: true # 安全需要
23 ports:
24 - containerPort: 8080
25 startupProbe:
26 httpGet:
27 path: /actuator/health/liveness
28 port: 8080
29 initialDelaySeconds: 5
30 periodSeconds: 5
31 failureThreshold: 30
32 timeoutSeconds: 5
33 livenessProbe:
34 httpGet:
35 path: /actuator/health/liveness
36 port: 8080
37 initialDelaySeconds: 5
38 periodSeconds: 10
39 failureThreshold: 3
40 timeoutSeconds: 5
41 readinessProbe:
42 httpGet:
43 path: /actuator/health/readiness
44 port: 8080
45 initialDelaySeconds: 5
46 periodSeconds: 10
47 failureThreshold: 3
48 timeoutSeconds: 5
49 resources:
50 limits:
51 cpu: 1000m
52 requests:
53 cpu: 100m
54 args: # 示範用 command line arguments 黎改變 Spring Boot behaviors
55 - --logging.level.root=DEBUG
56
57# 用 emptyDir 方法,令 /tmp 可以寫入
58 volumeMounts:
59 - name: empty-tmp-dir
60 mountPath: /tmp
61 volumes:
62 - name: empty-tmp-dir
63 emptyDir: {}
保存左個檔案之後執行:
kubectl apply -f k8s-deployment.yml
之後可以 port-forward 個 K8s Deployment
:
kubectl port-forward deployment/spring-boot-3-demo 8080
3.2.3 Service
當我地既 microservice 有多個 K8s Pod
行緊,我地最好整返個 K8s Service
,因為:
- K8s
Pod
呢樣野既性質係短暫/動態/流動既,佢地隨時可以因為唔同原因而增加減少,而每一個 K8s Pod
又有自己既 IP,但我地正常都會用 domain name 既 URL,而唔係喺個 downstream microservice 度配置 upstream microservice 既 IP address。
- K8s
Service
可以提供 load balancing 既功能(有可能係隨機分配既實現方式),自動分配 requests 去唔同既 K8s Pod
。
k8s-service.yml
:
1kind: Service
2apiVersion: v1
3metadata:
4 name: svc-spring-boot-3-demo
5 namespace: default
6spec:
7 type: ClusterIP
8 selector:
9 app: spring-boot-3-demo
10 ports:
11 - port: 8080
12 targetPort: 8080
13 protocol: TCP
保存左個檔案之後執行:
kubectl apply -f k8s-service.yml
之後可以 port-forward 個 K8s Service
:
kubectl port-forward service/svc-spring-boot-3-demo 8080
3.3 測試
可以檢查下 Spring Boot Actuator 既 HTTP REST endpoints:
1curl http://localhost:8080/actuator/health
2curl http://localhost:8080/actuator/health/liveness
3curl http://localhost:8080/actuator/health/readiness
4curl http://localhost:8080/actuator/info
5curl http://localhost:8080/actuator/env
6curl http://localhost:8080/actuator/beans
7curl http://localhost:8080/actuator/loggers
3.4 詳細解釋
- Spring Boot 本身具備 cloud awareness。
- 喺呢個測試項目裡面,我地用左 Spring Boot Actuator,而個 Spring Boot microservice 又喺 K8s 上面運行。
- Spring Boot 會 check 到有
KUBERNETES_SERVICE_HOST
、KUBERNETES_SERVICE_PORT
呢兩個 environment variables。
- 所以 Spring Boot 就知道佢而家喺 K8s 上面行緊,就會自動 expose
/actuator/health/liveness
、/actuator/health/readiness
呢兩個 HTTP REST endpoints。
- 我地配置 K8s 既
terminationGracePeriodSeconds
秒數唔應該少過我地配置既 Spring Boot graceful shutdown 既 spring.lifecycle.timeout-per-shutdown-phase
配置時限。
- Spring Boot 默認係會即時 shutdown,無視未完成既 HTTP requests。
- 我地當然唔想有 HTTP requests 突然被中斷,所以我地要指示 Spring Boot 用 graceful shutdown 方式,並且將 shutdown timeout 設置到一個合理既數值。
- 將
server.shutdown
設置做 graceful
。
spring.lifecycle.timeout-per-shutdown-phase
既默認值係 30
秒。
- 設置好之後,Spring Boot 會等到當時未完成既 HTTP requests 都完成曬或者到左 shutdown timeout 既時限先至 shutdown。
- K8s 都有默認
30
秒既 terminationGracePeriodSeconds
配置。
- 如果我地有配置到比
30
秒長既 Spring Boot shutdown timeout,咁我地都要配置埋 K8s 果個。
- 例子/測試方法
- 我地既 Spring Boot microservice 可以 expose 一個會
Thread.sleep(100_000)
(暫停 100
秒)既 HTTP REST endpoint。
- 然後設置 Spring Boot graceful shutdown、shutdown timeout 做
120
秒。
- 重新 build 個 Spring Boot Docker image。
- 然後喺部署 K8s 既 YAML 配置檔度設置 termination grace period 做
120
秒。
- 用
kubectl apply
部署個 Spring Boot microservice 既 K8s Deployment
。
- 我地 port-forward 其中一個
Pod
,然後用 Postman 或者 Bruno 去 call 個新 endpoint。
- 我地即刻執行
kubectl rollout restart deployment spring-boot-3-demo
。
- 我地會見到 Postman 或者 Bruno 可以執行個 HTTP request 長達
120
秒,到成功返回 HTTP response 之後 K8s 先會結束個 Pod
,然後用新既去取代佢。
- K8s 提供左唔同既 probes 配置去驗證個
Pod
到底正唔正常。
- 佢可以持續咁自動 call 個
Pod
既一啲 HTTP endpoints。
- 成功既定義係 HTTP response status code 要係
200
至 399
。
- Probes
- 如果 startup probe 既 HTTP endpoint fail 左,K8s 就會回收個
Pod
。
- 如果 liveness probe 既 HTTP endpoint fail 左,K8s 就會回收個
Pod
。
- 如果 readiness probe 既 HTTP endpoint fail 左,K8s 就會唔畀 traffic 去呢個
Pod
度。
initialDelaySeconds
係初始化既時候 K8s 應該等幾耐先開始驗證。
periodSeconds
係初始化完成之後 K8s 每隔幾耐需要驗證。
failureThreshold
係可以允許既失敗次數既上限。
- Spring Boot Actuator 提供左對應 K8s liveness、readiness probes 既 HTTP REST endpoints。
/actuator/health/liveness
可以用落 K8s 既 liveness probe。
/actuator/health/readiness
可以用落 K8s 既 readiness probe。
- 呢個 endpoint 返回 HTTP response status code
200
既話,即係表示個 HTTP server 已經準備好接受 HTTP requests。
- Liveness endpoint 返回正面既結果,唔代表 readiness endpoint 會返回正面既結果,因為個 HTTP server 未必準備好接受 HTTP requests。
- 如果 readiness endpoint 返回正面既結果,咁 liveness endpoint 都應該返回正面既結果。
- Spring Boot Actuator 冇提供對應 K8s startup probe 既 startup endpoint。
- 有兩個解決方法:
- 我地可以用返 Spring Boot Actuator 既 liveness endpoint 作為 K8s startup probe,然後配置一個好大既
failureThreshold
,例如 30
次,配合短既 periodSeconds
,例如 5
秒(即係最長 2.5
分鐘)。
- 呢個做法等於延續 K8s liveness probe 既驗證時間。
- 我地可以為 K8s liveness、readiness probes 配置一個好大既
initialDelaySeconds
,例如 180
秒,但呢個時間係固定既,時間越長就越會增加 scaling 既成本。
- 我地可以用 K8s
Deployment
既 replicas
配置黎指定需要既 Pod
數量。
- K8s 會幫我地管理
Pod
,盡可能保持我地要求既 Pod
數量。
- K8s 會幫我地自動創建一個 K8s
ReplicaSet
。
- 不過呢個方法得到既
Pod
既數量係固定既,唔會因應 Pod
container 既 CPU、memory、web requests 或者 events 既情況而自動增加減少。
- 比較好既做法係用 K8s
HorizontalPodAutoscaler
做 auto 既 horizontal scaling(需要 metrics server)。
- 亦都可以用 Kubernetes Event-driven Autoscaling(KEDA)。
3.5 注意事項
- 設置左
readOnlyRootFilesystem
做 true
既話,一定要令到 /tmp
有 volume mount,從而令 /tmp
可以寫入。
- 否則,如果 Spring Boot 既 embedded Tomcat 喺 runtime 既時候創建唔到目錄或者寫入唔到落 temp folder 既話,就會令成個 Spring Boot microservice 啟動失敗。
- 用 Docker Desktop 既 K8s 既話,有兩個方法:
- 用 K8s 正路既
emptyDir
方法。
- Dockerfile 用
VOLUME /tmp
instruction。
- 用 cloud 既 K8s 既話,有兩個方法:
- 用 K8s 正路既
emptyDir
方法。
- Dockerfile 用
VOLUME /tmp
instruction,但 containerd 既 ignore_image_defined_volumes
一定要配合佢而設置做 false
。
4 附錄
4.1 Snyk Java Dockerfile best practices
4.2 K8s pod lifecycle
4.3 Read-only file system /tmp
寫入問題
以下係如果我地冇整好 volume mount 落 /tmp
會造成既 startup error:
1Caused by: org.springframework.boot.web.server.WebServerException: Unable to create tempDir. java.io.tmpdir is set to /tmp
2 at org.springframework.boot.web.server.AbstractConfigurableWebServerFactory.createTempDir(AbstractConfigurableWebServerFactory.java:241)
3 at org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getWebServer(TomcatServletWebServerFactory.java:202)
4 at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.createWebServer(ServletWebServerApplicationContext.java:188)
5 at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:162)
6 ... 15 common frames omitted
7Caused by: java.nio.file.FileSystemException: /tmp/tomcat.8080.9381430298818445978: Read-only file system
8 at java.base/sun.nio.fs.UnixException.translateToIOException(Unknown Source)
9 at java.base/sun.nio.fs.UnixException.rethrowAsIOException(Unknown Source)
10 at java.base/sun.nio.fs.UnixException.rethrowAsIOException(Unknown Source)
11 at java.base/sun.nio.fs.UnixFileSystemProvider.createDirectory(Unknown Source)
12 at java.base/java.nio.file.Files.createDirectory(Unknown Source)
13 at java.base/java.nio.file.TempFileHelper.create(Unknown Source)
14 at java.base/java.nio.file.TempFileHelper.createTempDirectory(Unknown Source)
15 at java.base/java.nio.file.Files.createTempDirectory(Unknown Source)
16 at org.springframework.boot.web.server.AbstractConfigurableWebServerFactory.createTempDir(AbstractConfigurableWebServerFactory.java:235)
17 ... 18 common frames omitted
5 參考資料