Java后端技术栈
2021-05-23 12:45:48 0 举报
AI智能生成
Java后端技术栈, 整理知识点
作者其他创作
大纲/内容
基础知识
Java
JUC
多线程
线程池
锁
集合
IO
反射
泛型
异常
Servlet
面向对象
类
接口
抽象类
Java8新特性
lambda
stream
Optional类
default
类型注解
任意位置都可使用注解
泛型类型推断优化
省略后面<>
移除永久代
元空间
本地内存存储类元数据信息
日期
LocalDateTime
Instant
...
JVM
类加载机制
类的生命周期
加载
加载.class文件到内存, 生成java.lang.Class的对象
验证
验证class文件的正确性
准备
给静态变量分配内存, 设置零值
解析
把类的符号引用转化为直接引用
初始化
给静态变量赋真正的初值
使用
卸载
类加载器(组合)
类加载机制
全盘负责
加载一个类, 它的依赖和引用也负责一起加载
父类委托
加载一个类时, 先让父加载器先加载
缓存机制
加载过的Class会缓存起来
使用时, 先找缓存, 缓存没有再重新加载, 再存到缓存
这样, 修改一个class文件是, 要重启JVM, 才能生效
双亲委派机制
一个类加载器收到一个类加载请求时, 都会先委托父加载器加载, 父加载器加载不了, 才会自己去加载
优点
防止内存中出现多份相同的字节码文件
安全
缺点
SPI, Mysql驱动等会破坏双亲委派?
自定义类加载器
继承 ClassLoader
重写findClass方法
类加载方式
JVM启动时加载
Class.forName()
ClassLoader.loadClass()
JVM内存结构
总览图
java8
程序计数器
Program Counter Register
存储指向下一条指令的地址
多线程, CPU不断切换线程执行, 需要记录下一步执行的指令地址.
每个线程私有
生命周期和线程一样
它是唯一一个在 JVM 规范中没有规定任何 OutOfMemoryError 情况的区域
Java虚拟机栈
概述
Java Virtual Machine Stacks
每个线程在创建时都会创建一个虚拟机栈
内部保存一个个栈帧, 对应一次次方法的调用
是线程私有的, 生命周期和线程一样
它保存方法的局部变量、部分结果,并参与方法的调用和返回
不存在垃圾回收问题
Java虚拟机栈的大小是动态的或者是固定不变的
-Xss配置线程最大栈大小
存储单位
栈帧
Stack Frame
一个线程上正在执行的方法对应一个栈帧
栈运行原理
JVM对Java栈的操作
对栈帧的压栈和出栈
先进后出/后进先出原则
一个活动线程, 当前时间点, 只有一个栈帧有效, 就是当前执行的方法(栈顶栈帧),
字节码指令只对当前栈帧操作
如果调用了其他方法, 该方法对应的栈帧会被创建出来, 放在栈顶, 成为新的当前栈帧,先执行
当前栈帧方法执行结束, 会把结果返回给前一个栈帧, 丢弃执行完的栈帧, 前一个栈帧成为当前栈帧
栈帧的内部结构
局部变量表
存储方法参数和定义在方法内的局部变量
基本数据类型
对象引用
线程私有的, 线程安全的
最基存储单元
变量槽Slot
32位占用一个Slot
64位占用另个Slot
操作数栈
后进先出(Last-In-First-Out)的操作数栈
操作数栈,在方法执行过程中,根据字节码指令,往操作数栈中写入数据或提取数据,即入栈(push)、出栈(pop)
动态链接
运行时常量池的方法引用
方法返回地址
方法正常退出或异常退出的地址
一些附加信息
本地方法栈
Native Method
Java调用非Java代码的接口
Unsafe类
栈是运行时的单位,而堆是存储的单位
栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放、放在哪。
堆内存
线程共享
可以是物理上不连续的内存空间, 逻辑上连续即可
内存划分
新生代
新对象和没达到一定年龄的对象
Eden
8
S0
1
S1
1
-Xmn设置新生代大小
垃圾回收称为Minor GC
老年代
被长时间使用的对象,
大对象
需要大量连续内存空间的对象
内存大小一般比新生代大
垃圾回收称为Major GC
元空间
一些方法中的操作临时对象.
JDK1.8后使用武力内存, 前使用JVM内存
堆内存大小设置
-Xmx 和 -Xms 设置堆最大和最小内存大小
-Xmx 和 -Xms一般设置相同大小
为了在垃圾回收之后清理完堆内存之后, 不需要再重新计算堆内存大小, 提高性能
新生代和老年代比例默认1:2
–XX:NewRatio配置
新生代中的 Eden:From Survivor:To Survivor 的比例是 8:1:1
-XX:SurvivorRatio
JDK8 动态调整堆内存分配
-XX:+UseAdaptiveSizePolicy
对象在堆中的生命周期
1. 新创建对象 先到Eden区
JVM 会给对象定义一个对象年轻计数器(-XX:MaxTenuringThreshold)
2. Eden区内存不足时, 处罚Minor GC
3. JVM把存活的对象转移到Survivor区
对象年龄+1
4. Survivor也会Minor GC
每经历一次Minor GC 对象年龄+1
5. 如果分配的对象(内存大小, 字节)超过了-XX:PetenureSizeThreshold=<字节大小>,对象会直接被分配到老年代
默认0
大对象一般是指字符串和数组
内存分配策略
对象优先进入Eden区
大对象直接进入老年代
长期存活的对象进入老年代
动态对象年龄判定
空间分配担保
在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。
如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那么就要进行一次 Full GC。
方法区
线程共享
Java 虚拟机规范把方法区描述为堆的一个逻辑部分
Non-Heap(非堆)
存什么?
运行时常量池(Runtime Constant Pool)
放编译期生成的各种字面量
文本字符
final修饰的常量
符号引用
类和接口的全限定名
字段的名称和描述符
方法的名称和描述符
类信息
静态变量
JIT编译后的代码
内部结构
类型信息
域Field信息
方法Method信息
元空间1.8
参数设置
-XX:MetaspaceSize
-XX:MaxMetaspaceSize
类型信息、字段、方法、常量
优化手段
逃逸分析 Escape Analysis
一种可以有效减少 Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法
通过逃逸分析,Java Hotspot 编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。
分析对象的动态作用域
一个对象在方法中被定义后, 只在方法内部被使用, 没有被外部方法使用, 没有return
没有逃逸
被外部方法使用
逃逸
jdk6之后默认开启
-XX:+DoEscapeAnalysis
编译器对代码的优化
同步省略/锁消除
一个对象只被一个线程访问, 就不同步, 不加锁
标量替换
标量: 不能再分解的数据
聚合量: 包括其他聚合量和标量
通过逃逸分析确定对象不会被外部使用, 就将对象分解成一个个标量(成员变量), JVM不会创建该对象, 分配在栈帧或寄存器上
开启标量替代
-XX:+EliminateAllocations
查看标量替代情况
-XX:+PrintEliminateAllocations
栈上分配
正常对象时分配在堆上的, 对象没有被引用会触发GC
对象较多时, GC压力大
影响应用性能
通过逃逸分析, 确定对象不被外部使用, 通过标量替代将对象分解, 分配在栈上
这些对象所占的内存空间在出栈后会被销毁, 减少GC
TLAB
Thread Local Allocation Buffer
对Eden区继续划分, JVM为每个线程分配一个私有缓存区域
多线程同时分配内存时, 使用TLAB可以避免线程安全问题, 还能提高分配速度
为什么要有TLAB?
堆区线程共享, 对象创建在JVM中非常频繁, 并发环境划分内存空间线程不安全
为避免多线程操作同一地址, 需加锁, 影响分配速度
开启TLAB
-XX:UseTLAB
默认TLAB空间只占Eden区的1%
-XX:TLABWasteTargetPercent 设置百分比
TLAB分配失败, 就加锁保证原子性, 在Eden区分配内存
OOM的情况
JMM
线程间通讯
共享内存
消息传递
Java内存模型
图
重排序
哪些重排序
编译器
指令级
内存
数据依赖性
如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性
as-if-serial语义
不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。
编译器,runtime 和处理器都必须遵守 as-if-serial 语义。
编译器,runtime 和处理器都必须遵守 as-if-serial 语义。
编译器和处理器不会对存在数据依赖关系的操作做重排序
顺序一致性内存模型
处理器内存模型
内存屏障
为了保证内存可见性,java 编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序
指令类别
LoadLoad Barriers
StoreStore Barriers
LoadStore Barriers
StoreLoad Barriers
Store1; StoreLoad; Load2
该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令
happens-before
如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在 happens-before 关系
前一个操作对于后一个操作可见, 且前一个操作按顺序排在第二个操作之前
规则
程序顺序规则
前一个操作happens- before后续操作
监视器锁规则
解锁happens- before加锁
volatile 变量规则
volite的写happens- beforev votile的读
传递性
A happens- before B, B happens- before C
A happens- before C
GC
什么是垃圾
没有被使用的对象
如何确认垃圾
引用计数法
计数器
对象新增一个引用, 计数器+1, 引用消除计数器-1
计数器为0时对象可被回收
存在循环引用问题
可通过 Recycler 算法解决
可达性分析法
通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。
GC ROOTS对象
虚拟机栈中引用的对象
本地方法栈中引用的对象
方法区中类静态属性引用的对象
方法区中的常量引用的对象
什么时候回收垃圾
堆
Minor GC
新生代
Eden区满就触发
G1 GC
Major GC
老年代
CMS GC
Mixed GC
新生代和老年代
G1 GC
Full GC
整个堆和方法区
触发条件
调用 System.gc()
老年代空间不足
空间分配担保失败
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC
JDK 1.7 及以前的永久代空间不足
Concurrent Mode Failure
如何避免Full GC
尽量不创建过大的对象和数组
-Xmn128M调大新生代的大小
-XX:MaxTenuringThreshold=15调大对象的年龄阈值, 推迟进入老年代
默认15
方法区
常量池中的常量没有使用就会被回收
对常量池的回收和对类的卸载
类的卸载条件
该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。
加载该类的 ClassLoader 已经被回收。
该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。
引用类型
强引用
new
Object obj = new Object();
不会被回收
软引用
SoftReference
SoftReference<Object> sf = new SoftReference<Object>(obj);
内存不够时会被回收
弱引用
WeakReference
WeakReference<Object> wf = new WeakReference<Object>(obj);
一定会被回收, 存活到下次GC之前
虚引用
PhantomReference
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
对象被回收时收到一个系统通知
用途?
怎么回收垃圾
回收算法
标记-清除
图
标记存活对象, 清除未标记的对象
追踪
从GC ROOT查找存活的对象并标记
清除
再清除未标记的对象
不足
产生的内存碎片多, 导致无法存储大对象
整个过程对象不移动
标记和清除效率不高
标记-整理
图
存活的对象向一端移动, 清除 边界外的内存
从GC ROOT查找存活的对象并标记
对象移动
复制
图
内存分为大小相等两块
只使用一块, 一块满了之后, 将存活对象移动到另一块, 再清除全部内存
内存利用率不高, 只有一半
新生代, eden和survivor
分代
新生代
复制算法
老年代
标记-清除/标记-整理
用什么东西回收垃圾
大概图
Serial 收集器
串行
垃圾收集器和用户程序交替执行
会停顿
单线程
新生代
client模式
ParNew 收集器
Serial 收集器的多线程版本
新生代
Server模式
只有ParNew可和CMS配合工作
默认线程数和CPU数一样
-XX:ParallelGCThreads设置线程数
Parallel Scavenge 收集器
多线程
吞吐量优先?
Serial Old 收集器
Serial 收集器的老年代版本
Client 模式
Parallel Old 收集器
Parallel Scavenge 收集器的老年代版本。
CMS 收集器
CMS(Concurrent Mark Sweep)
标记-清除算法
老年代
流程
初始标记
标记一下 GC Roots 能直接关联到的对象
停顿
并发标记
进行 GC Roots Tracing 的过程
不停地
重新标记
为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
停顿
用户程序运行时, 标记可能错乱, 变动, 需要重新标记下
并发清除
清除垃圾
不停顿
缺点
吞吐量底
低停顿时间是以牺牲吞吐量为代价的
无法处理浮动垃圾
可能出现 Concurrent Mode Failure
浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾
浮动垃圾只能等待下一次GC
需要预留内存
标记清除产生内存碎片
无法存储大对象, 没有连续空间
需提前Full GC
G1收集器
G1 可以直接对新生代和老年代一起回收
G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。
流程
初始标记
并发标记
最终标记
筛选回收
性能调优
分析工具
jps -l
jstack -pid
jinfo -flags pid
Java System属性和JVM命令行参数
jmap
dump 文件
jmap -dump:live,format=b,file=/tmp/heap2.bin 2815
jmap -dump:format=b,file=/tmp/heap3.bin 2815
jmap -dump:format=b,file=/tmp/heap3.bin 2815
查看堆内对象
jmap -heap pid
查看堆的占用
jmap -histo 2815 | head -10
jstat
对Heap size和垃圾回收状况的监控
jstat -gcutil 2815 1000
查看gc次数
jstat -gc pid
jdb
jvm优化启动参数
-Xmx1g
堆最大内存
-Xms1g
堆最小内存
-Xmn341M
新生代内存
默认堆的1/4或1/3
-XX:NewRatio=4
新生代与老年代比例1:4
新生代占堆的1/5
与-Xmn128M选其一
-Xss256k
线程堆栈大小
-XX:MaxTenuringThreshold=15
新生代中的对象存活次数
G1默认15
CMS,默认6
一次Minor GC +1
-XX:SurvivorRatio=8
新生代 Eden区和Survivor区的比例8:2
-XX:PretenureSizeThreshold=3M
大于3M的对象直接进入老年代
默认为0
-XX:AutoBoxCacheMax=20000
加大Integer Cache
回收器
默认是G1
日志相关
-Xloggc:/home/gc.log
生成gc日志文件
-XX:+PrintGCDetails
gc日志
XX:+PrintGCDateStamps
gc日志打印日期
JMX
-Dcom.sun.management.jmxremote.port=7001 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1
dump
-XX:+HeapDumpOnOutOfMemoryError
发生OOM时自动dump到文件
XX:HeapDumpPath=/opt/heap.hprof
dump文件
查看dump文件
jhat heap.hprof
工具
https://gceasy.io/
分析gc日志
远程debug
-Xdebug -Xrunjdwp:transport=dt_socket,suspend=n,server=y,address=${debug_port}
TCP/IP
Linux常见命令
理论
CAP
SOLID
ACID
DDD
数据结构与算法
框架
ORM框架
Mybatis
Mybatis Plus
Spring Data Jpa
Spring Framework
Spring
SpringMVC
SpringBoot
Spring Security
Spring Cloud Alibaba
Spring Cloud Gateway
Nacos
sentinel
Seata
OpenFeign
Spring Cloud Stream
SkyWalking
Netty
Shiro
中间件
MQ
RocketMQ
Kafka
RabbitMQ
Dubbo
Zookeeper
web容器
Nginx
Tomcat
Undertow
Jetty
Sharding-JDBC
数据库
Redis
安装redis
数据结构
string
hash
set
zset
list
bitmap 布隆过滤器
命令
持久化
RDB
Redis DataBase 快照
触发方式
手动触发
save命令
阻塞主进程, 直到RDB完成
内存较大的话会长时间组设
不建议使用
bgsave命令
fork一个子进程
RDB由子进程完成
主进程仍可以写数据
阻塞只在fork阶段
自动触发bgsave
配置 save m n m秒有n次修改
主从复制
debug reload命令
shutdown命令
配置
save <seconds> <changes>
save "" 关闭RDB
# 如果持久化出错,主进程是否停止写入
stop-writes-on-bgsave-error yes
stop-writes-on-bgsave-error yes
RDB过程
fork一个bgsave子进程
开辟一个临时内存空间存放RDB文件
写时复制Copy-on-Write
有新写时, 主进程写入数据, 生成数据副本,
bgsave子进程把副本数据写入RDB文件
bgsave子进程把副本数据写入RDB文件
RDB完成替换旧的RDB文件
AOF
写后日志, 先写到内存, 再记录日志(命令 文本 磁盘)
实现AOF
命令追加append
执行一个命令 把命令追加到aof_buf 缓冲区
文件写入write
将aof_buf 缓冲区的内容写入aof文件(磁盘)
文件同步sync
写回策略
Always
同步每条命令
Everysec
每秒
No
由操作系统决定
配置
# appendonly参数开启AOF持久化
appendonly no
appendonly no
# 同步策略
# appendfsync always
appendfsync everysec
# appendfsync no
# appendfsync always
appendfsync everysec
# appendfsync no
# 重写触发配置
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
AOF重写
新建一个AOF文件, 合并冗余命令, 最终数据一致
后台进程bgrewriteaof完成
1. 主进程fork一个bgrewriteaof子进程(会阻塞)
2. 把主进程的内存拷贝一份给bgrewriteaof子进程
3. 执行命令, 再重写到新AOF文件
4. 有新写命令, 主进程写入旧的日志文件, 再保存命令到AOF重写缓冲区
5. 子进程重写完成, 提示主线程已完成重写操作
6. 主进程把AOF重写缓存区里的命令追加到新的AOF文件
7. 替换旧的AOF文件
一个拷贝,两处日志
旧文件 主进程
新文件 bgrewriteaof进程
RDB与AOF混合
4.0
第一次RDB全量快照
AOF记录两次快照之间产生的命令
第二次全量快照, 清空AOF日志
恢复数据
优先AOF
AOF文件不存在, 再加载RDB
集群
主从复制
概念
采用读写分离模式
读
主库, 从库都可读
写
写 只在主库, 再由主库->从库 (单向)
配置
在从库 replicaof 主库IP:PORT (5.0)
slaveof (5.0之前)
slaveof (5.0之前)
原理
全量复制
确立主从关系
从节点配置 replicaof 172.16.19.3 6379
三个阶段
主从库建立连接, 协商同步
从->主 psync ? -1
主->从 FULLRESYNC runID offset
主库发送RDB文件给从库
从库清空全部数据
加载RDB
主库把第二阶段过程中生成的新写命令(replication buffer), 发送给从库
从库接收
增量复制2.8
存在问题
节点故障 不会恢复
sentinel 哨兵模式
功能
主节点自动故障转移
监控 主从节点状态
主库发送INFO命令给从库, 获取从库列表
配置提供者
客户端初始化时, 获取主节点地址
通知
哨兵可以将故障转移的结果发送给客户端
哨兵集群搭建
发布订阅机制
主库有一个__sentinel__:hello的频道
哨兵发布ip 端口到此频道,
其他哨兵订阅
至少三个且奇数个sentinel节点
主库下线判断
主观下线
任何一个哨兵都可以监控探测, 做出自己的判断
当认为下线, 给其他哨兵发送 is-master-down-by-addr命令
其他哨兵也做出判断
sentinel monitor mymaster 172.16.251.15 9011 2
quorum=2
quorum=2
赞成票大于2, 就是客观下线了
客观下线
哨兵集群共同决定master节点是否下线
哨兵集群选举
Raft选举算法
选举的票数>=num(sentinels)/2+1, 成为领导者
没有超过, 继续选举
成为Leader必要条件
半数以上赞成票
票数 >=quorum
选出新主库
过滤下线的, 不健康的
salve-priority 优先级最高的
选择复制偏移量最大, 复制最完整的从节点
定时监控任务
每个哨兵节点每10秒会向主节点和从节点发送info命令获取最拓扑结构图
每个哨兵节点每隔2秒会向redis数据节点的指定频道上发送该哨兵节点对于主节点的判断以及当前哨兵节点的信息,同时每个哨兵节点也会订阅该频道,来了解其它哨兵节点的信息及对主节点的判断
每隔1秒每个哨兵会向主节点、从节点及其余哨兵节点发送一次ping命令做一次心跳检测
cluster集群模式
哈希槽 Hash Slot
有16384(即2的14次方)个哈希槽
每个key通过CRC16校验后对16383取模来决定放置哪个槽。
Keys hash tags
用来将多个(相关的)key分配到相同的hash slot中
{user1000}.following和{user1000}.followers
计算user1000
CLUSTER NODES
每个节点 会生成一个唯一ID
查看集群状态以及每个节点的信息
d289c575dcbc4bdd2931585fd4339089e461a27d 127.0.0.1:6381 master - 1318428931 1318428931 3 connected 2730-4095
node id, address:port, flags, last ping sent, last pong received, configuration epoch, link state, slots.
Gossip协议
扩容
添加新节点
cluster meet 新节点ip:端口
redis-trib add node添加
默认添加主节点
数据迁移
存在的问题
缓存穿透
缓存和数据库都没有数据
恶意请求
解决
参数校验
请求不存在的key, 返回null, 设置过期时间, 如30秒, 防止数据恢复请求拿不到数据
布隆过滤器: 将数据主键放到布隆过滤器, 不存在的key一定不存在直接返回null
IP白名单
缓存击穿
缓存没有数据, 数据库有数据
热点key过期了, 大量请求打到数据库
解决
热点key永不过期
接口限流, 降级熔断
加互斥锁?
缓存雪崩
大量key同时过期了
解决
key的过期时间不要设置都一样, 加个随机数, 这样可以不同时过期
热点key永不过期
cluster集群, 数据分片, 这样不会所有服务都down调
缓存预热
提前刷新数据库数据到缓存
解决
项目启动时刷新到缓存
留个接口 按钮
缓存污染(或满了)
某些key只用了一两次, 没过期, 一直站着内存, 内存越用越少
解决
淘汰策略
数据一致性
数据库与缓存的数据一致性问题, 先更新缓存还是数据库?
方式一
1. 更新mysql数据, 再删除key, 让缓存失效
2. 查询时, 查缓存没有, 再查mysql, 再把数据更新到缓存
把更新缓存的操作交给用户
方式二
使用canal binlog
1. canal监听mysql数据update, insert, delete
2. 发送到mq
3. 消费mq, 更新缓存
4. 失败重试
key 过期策略
定期删除
定期去除一批key, 判断是否过期
配置 lazyfree-lazy-eviction = yes, 后台删除(4.0+)
惰性删除
使用时才判断是否过期
内存不足时淘汰策略
volatile
volatile-random
随机
volatile-ttl
过期时间最小的
volatile_lru
Least Recently Used 最近最少使用
volatile-lfu
Least Frequently Used 最不经常使用
allkeys
allkeys-random
allkeys-lfu
allkeys-fru
no-eviction
不淘汰
用途
缓存
计数
分布式锁
redis集群之间数据同步
MySQL
MongoDB
ES
canl
常用工具
Git
Maven
Docker
Jenkins
K8S
设计模式
0 条评论
下一页