JAVA学习手册
2021-11-30 23:54:28 0 举报
AI智能生成
JAVA学习手册
作者其他创作
大纲/内容
常用工具
Linux
Shell
shell简介
cat /etc/Shells
/bin/bash
/bin/sh
……
运行脚本
bash hello.sh
不需要执行权限
./hello.sh
需要执行权限
内建命令
type
确定内建命令
.
执行程序
alias
别名
unalias
取消别名
bg
放到后台
fg
调到前台
jobs
查看暂停的任务
echo
打印字符
export
使变量被子shell识别
local
声明局部变量
ulimit
显示并设置进程资源限度
bash预设的环境变量
BASH
BASH_VERSION
CDPATH
快捷目录
EUID
当前用户的UID
FUNCNAME
HOSTNAME
LANG
PWD
PATH
特殊变量
位置参数
$0
脚本本身
$1
第1个参数
脚本或命令返回值
$?
0:正常退出
数组
定义
declare -a array
array[0] = 0
取值
echo ${array[0]}
echo ${array[@]}
echo ${array[*]}
长度
echo ${#array[@]}
只读变量
readonly a=100
引用
双引号
部分引用
会转义
echo "$he"
100
单引号
全引用
所有字符都是字面意思
echo '$he'
$he
反引号
系统命令
特殊字符
*
任意长度字符串
?
任意单个字符
[]
DevOps
maven
git
git 基础
2.1 创建版本库
git init
git init [project-name]
git clone <url>
下载制定标签
git clone --branch v1.6.5.7 https://github.com/ManaPlus/ManaPlus.git
2.2 添加/删除/重命名文件
新增
git add <file1> <file2> ...
git add <dir>
git add .
git add -p
git commit -m "提交内容"
git commit -am
git commit -v
删除
git rm --cached <file>
git rm <file1> <file2> ...
重命名
git mv <file-original> <file-renamed>
2.3 时光机穿梭
HEAD指针
HEAD 当前版本
HEAD^ 上一个版本
HEAD^^ 上上一个版本
HEAD~100 上100版本
git show d921970^2
文件只有两种状态
已跟踪
未更新
已修改
已暂存
未跟踪
git status
git status -s
git status --short
跟踪新文件
git add ./fileDir/fileName
忽略某些文件
cat .gitignore 查看
.gitignore文件也支持glob匹配模式
git diff 查看更改
git diff --staged查看已经暂存(还未commit)的文件里的更改
移除文件
git rm [-f]
git rm [-f]
git rm --cached fileName
从git仓库移除文件,但是保留在当前工作目录中
git rm * -r删除所有文件(包括目录)
git mv beforeName afterName
2.3 查看提交历史
git log 可以查看提交历史
git log [-p]
查看详细差异
git log [-n]
git log --author=wuxiaolan
git log
-p
-2
--stat
--pretty
oneline
short
full
fuller
format
--graph
git log --stat
git log --pretty=oneline/short/full/fuller
git log --grep keyword
git log --graph
查看提交树
查看提交树
git log -S [keyword]
git log [tag] HEAD --pretty=format:%s
git log [tag] HEAD --grep feature
git log --follow [file]
git whatchanged [file]
git log -p [file]
git log -5 --pretty --oneline
git shortlog -sn
git blame [file]
显示暂存区和工作区的代码差异
git diff --cached [file]
git diff HEAD
git diff [first-branch]...[second-branch]
git diff --shortstat "@{0 day ago}"
git show [commit]
git show --name-only [commit]
git show [commit]:[filename]
git reflog
git log --abbrev-commit --pretty=oneline
git show HEAD@{5}
git show master@{yesterday}
git show HEAD@{2.months.ago}
多点
git log ^refA refB
git log refB --not refA
双点
git log origin/master..HEAD
git log master..experiment
三点
git log --left-right master...experiment
gitk 查看git历史
git log的可视化版本
git log的可视化版本
2.4 撤销操作
取消已经暂存的文件
git reset HEAD [file]
git reset .撤销上次add
修改最后一次的提交
git commit --amend
修改最后一次提交
版本回退/前进
git reset --hard [HEAD^]
暂存区的修改回退到工作区
git reset HEAD [readme.txt]
git checkout [file]
git checkout [commit] [file]
git checkout .
git reset [file]
git reset --hard
git reset [commit]
git reset --hard [commit]
git reset --keep [commit]
git revert [commit]
git commit --amend
2.5 远程仓库的使用
git fetch [remote]
git push origin localBranchName:remoeBranchName
把本地的新建的分支推送到远程去
git remote show origin
服务器有哪些分支本地是没有更新的等等
关联远程库
git remote add origin [git@github.com:michaelliao/learngit.git]
git remote add origin [git@github.com:michaelliao/learngit.git]
git remote -v
git remote show [remote]
git remote add [shortname] [url]
git pull [remote] [branch]
git push [remote] [branch]
git push [remote] --force
git push [remote] --all
git remote set-url origin git@192.168.6.70:res_dev_group/test.git
变更项目地址
变更项目地址
2.6 标签操作
git tag
标签的两种类型
轻量级的
git tag tagName
含附注的
git tag -a tagName -m tagDesc
git tag -a tagName sha-1值
给以前的提交补充tag
git tag -s v0.2 -m "signed version 0.2 released" fec145a
-s用私钥签名一个标签
-s用私钥签名一个标签
git tag -d <tag>
删除本地标签
删除本地标签
git push origin :refs/tags/v0.9
删除远程库标签
删除远程库标签
git tag <tag> <commit>
git show [tag]
git push <remote> <tag>
git push <remote> --tags
git tag -l 'v0.1.*'
新建
git tag v0.2.1-light
2.7 技巧和窍门
git 后连按两次TAB,能列出所有匹配的可用命令建议
git 命令的部分命令,再按TAB,能自动补全git命令
git co(自动丿配git commit/config,建议直接输git com+TAB
配置
git config --list
显示当前的Git配置
显示当前的Git配置
Git别名
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.ci commit
git config --global alias.st status
配置帐号密码
$ git config --global user.name "Your Name"
$ git config --global user.email "email@example.com"
$ git config --global user.email "email@example.com"
更换默认的编辑器
git config --global core.editor "vim"
关闭自动换行
git config --global core.autocrlf false
git config --global core.safecrlf true
git config –global push.default matching
git config http.postBuffer 524288000
生成密钥
ssh-keygen -t rsa -C "noogel666@gmail.com"
git config --global core.editor "/Applications/Sublime\ Text.app/Contents/SharedSupport/bin/subl -n -w"
代码提交
git commit -m <message>
git commit <file1> <file2]> ... -m [message]
git commit -a
git commit -v
git commit --amend -m [message]
git commit --amend [file1] [file2] ...
JenKins
docker
JDK源码
基本数据类型
Object
Byte
short
Integer
Long
Float
Double
Boolean
Character
String源码
集合
Collection
List
ArrayList
LinkList
Vector
CopyOnWriteArrayList
Set
HashSet
TreeSet
LinkedHashSet
queue
Deque
ArrayDeque
BlockingDeque
BlockingQueue
Map
HashMap
LinkHashMap
TreeMap
ConcurrentHashMap
设计模式
软件架构设计原则
开闭原则
对外扩展,对修改关闭
依赖倒置原则
通过抽象使各个类或者模块互不影响,实现松耦合
单一职责原则
一个类接口方法只做一件事情
接口隔离原则
尽量保存接口的纯洁性,客户端不应该依赖它不需要的接口
迪米特原则
一个对象对其他对象保持最少的了解,又叫最少知道原则,尽量降低类与类之间的耦合
里氏替换原则
子类可以扩展父类功能,但不能改变父类原有功能
合成复用原则
是指尽量使用对象组合(has-a)/聚合(contanis-a),而不是继承关系达到软件复用的目的
创建型
工厂模式
简单工厂模式
Calendar
LoggerFactory
工厂方法模式
抽象工厂模式
单例模式
饿汉式单例模式
懒汉式单例模式
双重检测模式
静态内部类单例
反射破坏单例
序列化破坏单例
枚举式单例
容器式单例
ThreadLocal实现
原型模式
通过拷贝原型创建新对象
scope="prototype"
JSON.parseObject()
实现Cloneable接口
序列化深度克隆
建造者模式
通常采用链式编程
StringBuilder
MyBatis
CacheBuilder
SqlSessionFactoryBuilder
结构性
适配器模式
转换委托将一种接口转化为另一种符合需求接口
门面模式
提供统一接口用来访问子系统中一群接口
装饰器模式
不改变原有对象的基础之上,将功能附加到对象上
代理模式
静态代理
动态代理
JDK实现InvocationHandler
CGlib动态代理
与JDK动态代理区别
JDK动态代理实现了被代理对象的接口,CGLIB代理继承了被代理对象
都在运行期生成字节码,JDK直接写class字节码,CGLIB使用ASM框架写字节码
JDK动态代理通过反射机制,CGlib动态代理使用FastClass机制,CGlib执行效率高
享元模式
对象池的一种实现,缓存共享对象,减少内存消耗
String,Integer,Long都使用了享元模式
组合模式
单个对象和组合对象用相同接口表示,具有一致性
桥接模式
通过组合的方式建立两个类之间联系
行为型模式
策略模式
同一行为在不同场景下不同实现
模板模式
定义一个操作中算法框架,将步骤延迟到子类中
委派模式
负责任务调用分配代理
责任链模式
将链中每一个节点看作对象,每个节点处理请求均不同,自动维护下一个节点
迭代器模式
顺序访问对象元素集合的方法,一致性方法
命令模式
对命令封装,通过中间件解耦命令请求和执行
观察者模式
发布订阅模式,主题变更通知所有订阅者
Observable
Guava Api
访问者模式
将数据结构和数据操作分离模式
状态模式
内部状态发生改变时改变其行为
Lifecyle
备忘录模式
存储各个历史快照方便恢复
中介者模式
调解者模式
Timer
解释器模式
按照规定语法进行解析
Pattern
框架解析
Spring
编程思想
OOP
Object Oriented Programming(面向对象编程)
封装
继承
多态
BOP
Bean Oriented Programming(面向Bean编程)
AOP
Aspect Oriented Programming(面向切面编程)
IOC
Inversion of Control(控制反转)
DI/DL
Dependency Injection(依赖注入)/Dependency Lookup(依赖查找)
核心容器
spring-beans
spring-core
spring-content
spring-express
AOP
spring-aop
spring-aspects
spring-instrument
数据访问与集成
spring-jdbc
spring-tx
spring-orm
spring-jms
spring-oxm
web组件
spring-web
spring-webmvc
spring-websocket
spring-webflux
通信报文
spring-message
集成测试
spring-test
集成兼容
spring-framework-bom
注解编程主键
配置组件
@Configuration(配置容器)
@CompontentScan(扫描该包下的配置类)
@Scope(用于指定scope的作用域)
@Lazy(延迟初始化)
@Conditional(条件判断注入bean)
@Import(导入外部资源)
生命周期
@PostConstruct(用户指定初始化方法)
@PreDestory(用于指定销毁方法)
@DependsOn(定义Bean初始化及销毁时的顺序)
赋值组件
@Component(泛指组件)
@Service(业务层)
@Controller(控制层)
@Repository(数据访问层)
@Value(普通数据类型赋值)
@Autowired(按类型装配)
@PropertySource(读取配置文件赋值)
@Qualifier(存在多个实例配合使用)
@Primary(bean首选者)
@Resource(按名称装配)
织入组件
ApplicationContextAware(通过上下文环境)
BeanDefinitionRegistryPostProcessor
切面组件
@EnableTransactionManagement(添加对事务管理支持)
@Transaction(配置声明式事务信息)
SpringBoot
Mybatis
分布式高并发
并发编程基础
线程基础
实现线程
实现 Runnable 接口
public class RunnableThread implements Runnable {
@Override
public void run() {
System.out.println('用实现Runnable接口实现线程');
}
}
@Override
public void run() {
System.out.println('用实现Runnable接口实现线程');
}
}
继承 Thread 类
public class ExtendsThread extends Thread {
@Override
public void run() {
System.out.println('用Thread类实现线程');
}
}
@Override
public void run() {
System.out.println('用Thread类实现线程');
}
}
线程池创建线程
DefaultThreadFactory
线程池和Callable 也可以创建线程,但本质上也是通过前两种基本方式实现的线程创建
有返回值的 Callable 创建线程
定时器 Timer
class TimerThread extends Thread {
//具体实现
}
//具体实现
}
其他方法
/**
*描述:匿名内部类创建线程
*/
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}).start();
*描述:匿名内部类创建线程
*/
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}).start();
优先选择通过实现 Runnable 接口的方式来创建线程
启动停止线程
调用 Thread 类的 start() 方法,并在 run() 方法中定义需要执行的任务
interrupt 停止线程
方法签名抛异常,run() 强制 try/catch
volatile 在某些特殊的情况下,比如线程被长时间阻塞的情况,就无法及时感受中断,所以 volatile 是不够全面的停止线程的方法
线程六种状态
New(新创建)
Runnable(可运行)
Blocked(被阻塞)
Waiting(等待)
Timed Waiting(计时等待)
Terminated(被终止)
状态之间切换
子主题
wait/notify/notifyAll
wait 必须在 synchronized 保护的同步代码中使用
wait/notify/notifyAll 被定义在 Object 类
sleep 定义在 Thread 类中
生产者消费者模式
BlockingQueue 实现生产者消费者模式
Condition 实现生产者消费者模式
wait/notify 实现生产者消费者模式
线程安全
线程不安全情况
运行结果错误
发布和初始化导致线程安全问题
活跃性问题
死锁、活锁和饥饿
哪些场景需要注意线程安全问题
访问共享变量或资源
依赖时序的操作
不同数据之间存在绑定关系
对方没有声明自己是线程安全的
性能问题
上下文切换
缓存失效
协作开销
线程池
线程池的参数
corePoolSize核心线程数
maxPoolSize最大线程数
keepAliveTime+时间单位
ThreadFactory线程工厂
workQueue
Handler
创建线程流程
拒绝策略
AbortPolicy
直接抛出一个类型为 RejectedExecutionException 的 RuntimeException
DiscardPolicy
当新任务被提交后直接被丢弃掉,也不会给你任何的通知
DiscardOldestPolicy
丢弃任务队列中的头结点,通常是存活时间最长的任务
CallerRunsPolicy
把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务
新提交的任务不会被丢弃
谁提交任务谁就要负责执行任务,这样提交任务的线程就得负责执行任务,而执行任务又是比较耗时的,在这段期间,提交任务的线程被占用,也就不会再提交新的任务,减缓了任务提交的速度,相当于是一个负反馈。在此期间,线程池中的线程也可以充分利用这段时间来执行掉一部分任务,腾出一定的空间,相当于是给了线程池一定的缓冲期
常见的线程池
FixedThreadPool
核心线程数和最大线程数
CachedThreadPool
线程数可以无限增加的(实际最大可以达到 Integer.MAX_VALUE,为 2^31-1)
ScheduledThreadPool
定时或周期性执行任务
SingleThreadExecutor
使用唯一的线程去执行任务,原理和 FixedThreadPool 是一样的,只不过这里线程只有一个
适合用于所有任务都需要按被提交的顺序依次执行的场景
SingleThreadScheduledExecutor
是 ScheduledThreadPool 的一个特例,内部只有一个线程
ForkJoinPool
适合执行可以产生子任务的任务
有一个共用的任务队列之外,每个线程还有一个对应的双端队列 deque,这时一旦线程中的任务被 Fork 分裂了,分裂出来的子任务放入线程自己的 deque 里,而不是放入公共的任务队列中
阻塞队列
FixedThreadPool
LinkedBlockingQueue
容量为 Integer.MAX_VALUE 的 LinkedBlockingQueue,可以认为是无界队列
CachedThreadPool
SynchronousQueue
一旦有任务被提交就直接转发给线程或者创建新线程来执行,而不需要另外保存它们
ScheduledThreadPool
DelayedWorkQueue
内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构
SingleThreadExecutor
LinkedBlockingQueue
容量为 Integer.MAX_VALUE 的 LinkedBlockingQueue,可以认为是无界队列
SingleThreadScheduledExecutor
DelayedWorkQueue
内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构
自定义线程池
线程的平均工作时间所占比例越高,就需要越少的线程
线程的平均等待时间所占比例越高,就需要越多的线程
针对不同的程序,进行对应的实际测试就可以得到最合适的选择
关闭线程池
shutdown()
isShutdown()
isTerminated()
awaitTermination()
shutdownNow()
锁
偏向锁/轻量级锁/重量级锁
三种锁特指 synchronized 锁的状态,通过在对象头中的 mark word 来表明锁的状态
偏向锁 无竞争打个标记 轻量级锁短时间锁竞争采用cas 重量级锁有实际竞争
无锁→偏向锁→轻量级锁→重量级锁
可重入锁/非可重入锁
ReentrantLock
共享锁/独占锁
读写锁
公平锁/非公平锁
悲观锁/乐观锁
悲观锁:synchronized 关键字和 Lock 接口 悲观锁适合用于并发写入多、临界区代码复杂、竞争激烈等场景
synchronized
monitorenter
monitorexit
乐观锁:原子类 采用CAS AtomicInteger 乐观锁适用于大部分是读取,少部分是修改的场景,也适合虽然读写都很多,但是并发并不激烈的场景
自旋锁/非自旋锁
可中断锁/不可中断锁
synchronized 不可中断锁
ReentrantLock 中断锁
synchronized 和 Lock比较
相同点
synchronized 和 Lock 都是用来保护资源线程安全的。
都可以保证可见性
synchronized 和 ReentrantLock 都拥有可重入的特点
不同点
用法区别
synchronized 关键字可以加在方法上,同步块
Lock 接口必须显示用 Lock 锁对象开始加锁 lock() 和解锁 unlock(),并且一般会在 finally 块中确保用 unlock() 来解锁,以防发生死锁
加解锁顺序不同
synchronized 解锁的顺序和加锁的顺序必须完全相反
synchronized 锁不够灵活
Lock 类在等锁的过程中,如果使用的是 lockInterruptibly 方法,那么如果觉得等待的时间太长了不想再继续等待,可以中断退出,也可以用 tryLock() 等方法尝试获取锁,如果获取不到锁也可以做别的事,更加灵活
synchronized 锁只能同时被一个线程拥有,但是 Lock 锁没有这个限制
synchronized 是内置锁,由 JVM 实现获取锁和释放锁的原理,还分为偏向锁、轻量级锁、重量级锁。
可以设置公平/非公平
synchronized 不能设置
性能区别
Lock常用方法
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
并发容器
HashMap
ConcurrentHashMap
JDK7 数组+链表
JDK8 数组+链表+红黑树
当链表长度大于或等于阈值(默认为 8)的时候,如果同时还满足容量大于或等于 MIN_TREEIFY_CAPACITY(默认为 64)的要求,就会把链表转换为红黑树。
由于删除或者其他原因调整了大小,当红黑树的节点小于或等于 6 个以后,又会恢复为链表形态
Hashtable
采用synchronized
CopyOnWriteArrayList
适用场景
读操作可以尽可能的快,而写即使慢一些也没关系
读多写少
读写规则
读写锁的规则
对读写锁规则的升级
特点
当容器需要被修改的时候,不直接修改当前容器,而是先将当前容器进行 Copy,复制出一个新的容器,然后修改新的容器,完成修改之后,再将原容器的引用指向新的容器
迭代期间允许修改集合内容
缺点
内存占用问题
在元素较多或者复杂的情况下,复制的开销很大
数据一致性问题
阻塞队列
BlockingQueue
无界队列LinkedBlockingQueue
有界队列ArrayBlockingQueue
常用方法
add添加一个元素
remove删除元素
element返回队列的头部节点,但是并不删除
offer
用来插入一个元素,并用返回值来提示插入是否成功。如果添加成功会返回 true,而如果队列已经满了,此时继续调用 offer 方法的话,它不会抛出异常,只会返回一个错误提示:false
poll
移除并返回队列的头节点。但是如果当队列里面是空的,没有任何东西可以移除的时候,便会返回 null 作为提示
peek
返回队列的头元素但并不删除。如果队列里面是空的,它便会返回 null 作为提示
带超时时间的 offer 和 poll
put
插入元素。通常在队列没满的时候是正常的插入,但是如果队列已满就无法继续插入,这时它既不会立刻返回 false 也不会抛出异常,而是让插入的线程陷入阻塞状态,直到队列里有了空闲空间,此时队列就会让之前的线程解除阻塞状态,并把刚才那个元素添加进去
take
获取并移除队列的头结点。通常在队列里有数据的时候会正常取出数据并删除;但是如果执行 take 的时候队列里无数据,则阻塞,直到队列里有数据;一旦队列里有数据了,就会立刻解除阻塞状态,并且取到数据
常见阻塞队列
ArrayBlockingQueue
有界队列,其内部是用数组存储元素的,利用 ReentrantLock 实现线程安全
ArrayBlockingQueue 实现并发同步的原理就是利用 ReentrantLock 和它的两个 Condition,读操作和写操作都需要先获取到 ReentrantLock 独占锁才能进行下一步操作。进行读操作时如果队列为空,线程就会进入到读线程专属的 notEmpty 的 Condition 的队列中去排队,等待写线程写入新的元素;同理,如果队列已满,这个时候写操作的线程会进入到写线程专属的 notFull 队列中去排队,等待读线程将队列元素移除并腾出空间
LinkedBlockingQueue
无界队列内部用链表实现的 BlockingQueue。如果我们不指定它的初始容量,那么它容量默认就为整型的最大值 Integer.MAX_VALUE
SynchronousQueue
容量为 0,所以没有一个地方来暂存元素,导致每次取数据都要先阻塞,直到有数据被放入;同理,每次放数据的时候也会阻塞,直到有消费者来取
PriorityBlockingQueue
支持优先级的无界阻塞队列
DelayQueue
无界队列 具有“延迟”的功能,可以设定让队列中的任务延迟多久之后执行
放入的元素必须实现 Delayed 接口,而 Delayed 接口又继承了 Comparable 接口,所以自然就拥有了比较和排序的能力
非阻塞队列ConcurrentLinkedQueue
使用 CAS 非阻塞算法 + 不停重试,来实现线程安全,适合用在不需要阻塞功能,且并发不是特别剧烈的场景
线程池对于阻塞队列的选择
功能 容量 能否扩容 内存结构 性能
原子类
一组操作要么全都操作成功,要么全都失败,不能只操作成功其中的一部分
AtomicInteger、AtomicLong、AtomicBoolean
AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
AtomicReference、AtomicStampedReference、AtomicMarkableReference
AtomicIntegerfieldupdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
LongAdder、DoubleAdder
LongAccumulator、DoubleAccumulator
如果我们的场景仅仅是需要用到加和减操作的话,那么可以直接使用更高效的 LongAdder,但如果我们需要利用 CAS 比如 compareAndSet 等操作的话,就需要使用 AtomicLong 来完成
synchronized 和 AtomicInteger
保证线程安全
synchronized 背后的 monitor 锁
原子类利用了 CAS
使用范围
粒度
性能
悲观锁和乐观锁
ThreadLocal
使用场景
保存每个线程独享的对象
每个线程内需要独立保存信息
Thread ThreadLocal ThreadLocalMap
一个 Thread 里面只有一个ThreadLocalMap ,而在一个 ThreadLocalMap 里面却可以有很多的 ThreadLocal,每一个 ThreadLocal 都对应一个 value
ThreadLocalMap 解决 hash 冲突的方式是不一样的,它采用的是线性探测法。如果发生冲突,并不会用链表的形式往下链,而是会继续寻找下一个空的格子
在使用完了 ThreadLocal 之后,我们应该手动去调用它的 remove 方法,目的是防止内存泄漏的发生
Future
Callable 和 Runnable 区别
方法名,Callable 规定的执行方法是 call(),而 Runnable 规定的执行方法是 run()
返回值,Callable 的任务执行后有返回值,而 Runnable 的任务执行后是没有返回值的
抛出异常,call() 方法可抛出异常,而 run() 方法是不能抛出受检查异常的;
和 Callable 配合的有一个 Future 类,通过 Future 可以了解任务执行情况,或者取消任务的执行,还可获取任务执行的结果,这些功能都是 Runnable 做不到的,Callable 的功能要比 Runnable 强大
CompletableFuture
CompletableFuture 的 runAsync 方法,这个方法会异步的去执行任务
线程协作
Semaphore 信号量
acquire() 能够响应中断
acquireUninterruptibly()
tryAcquire()
availablePermits()查询可用许可证的数量
获取和释放的许可证数量尽量保持一致,否则比如每次都获取 2 个但只释放 1 个甚至不释放,那么信号量中的许可证就慢慢被消耗完了,最后导致里面没有许可证了,那其他的线程就再也没办法访问了
在初始化的时候可以设置公平性,如果设置为 true 则会让它更公平,但如果设置为 false 则会让总的吞吐量更高。
信号量是支持跨线程、跨线程池的
CountDownLatch
一个线程等待其他多个线程都执行完毕,再继续自己的工作
多个线程等待某一个线程的信号,同时开始执行
CyclicBarrier
执行动作 barrierAction
CyclicBarrier 和 CountDownLatch 的异同
都能阻塞一个或一组线程,直到某个预设的条件达成发生,再统一出发
作用对象不同:CyclicBarrier 要等固定数量的线程都到达了栅栏位置才能继续执行,而 CountDownLatch 只需等待数字倒数到 0,也就是说 CountDownLatch 作用于事件,但 CyclicBarrier 作用于线程;CountDownLatch 是在调用了 countDown 方法之后把数字倒数减 1,而 CyclicBarrier 是在某线程开始等待后把计数减 1。
可重用性不同:CountDownLatch 在倒数到 0 并且触发门闩打开后,就不能再次使用了,除非新建一个新的实例;而 CyclicBarrier 可以重复使用,在刚才的代码中也可以看出,每 3 个同学到了之后都能出发,并不需要重新新建实例。CyclicBarrier 还可以随时调用 reset 方法进行重置,如果重置时有线程已经调用了 await 方法并开始等待,那么这些线程则会抛出 BrokenBarrierException 异常
执行动作不同:CyclicBarrier 有执行动作 barrierAction,而 CountDownLatch 没这个功能
Condition接口
signalAll() 会唤醒所有正在等待的线程,而 signal() 只会唤醒一个线程
lock.lock() 对应进入 synchronized 方法
condition.await() 对应 object.wait()
condition.signalAll() 对应 object.notifyAll()
lock.unlock() 对应退出 synchronized 方法
condition.await() 对应 object.wait()
condition.signalAll() 对应 object.notifyAll()
lock.unlock() 对应退出 synchronized 方法
Java 内存模型
JVM 内存结构
堆区(Heap)
虚拟机栈(Java Virtual Machine Stacks)
方法区(Method Area)
本地方法栈(Native Method Stacks)
程序计数器(The PC Register)
运行时常量池(Run-Time Constant Pool)
JMM(Java Memory Model,Java 内存模型)
处理器、缓存、并发、编译器有关。它解决了 CPU 多级缓存、处理器优化、指令重排等导致的结果不可预期的问题
重排序
提高处理速度
编译器优化
CPU 重排序
内存的“重排序”
原子性
原子操作是指一系列的操作,要么全部发生,要么全部不发生,不会出现执行一半就终止的情况
除了 long 和 double 之外的基本类型(int、byte、boolean、short、char、float)的读/写操作,都天然的具备原子性
long 和 double 的值需要占用 64 位的内存空间,而对于 64 位值的写入,可以分为两个 32 位的操作来进行。
这样一来,本来是一个整体的赋值操作,就可能被拆分为低 32 位和高 32 位的两个操作。
如果在这两个操作之间发生了其他线程对这个值的读操作,就可能会读到一个错误、不完整的值
这样一来,本来是一个整体的赋值操作,就可能被拆分为低 32 位和高 32 位的两个操作。
如果在这两个操作之间发生了其他线程对这个值的读操作,就可能会读到一个错误、不完整的值
所有引用 reference 的读/写操作
加了 volatile 后,所有变量的读/写操作(包含 long 和 double
java.concurrent.Atomic 包中的一部分类的一部分方法是具备原子性的,比如 AtomicInteger 的 incrementAndGet 方法
内存可见性
volatile synchronized、Lock
happens-before
单线程规则
锁操作规则(synchronized 和 Lock 接口等)
volatile 变量规则
线程启动规则
线程 join 规则
中断规则
并发工具类的规则
volatile
volatile 不会发生上下文切换等开销很大的情况,不会让线程阻塞
保证可见性
禁止重排序
单例模式的双重检查锁模式须加 volatile
CAS 原理
Compare-And-Swap
ABA 问题
CAS 并不能检测出在此期间值是不是被修改过,它只能检查出现在的值和最初的值是不是一样
AtomicStampedReference
ABA 问题、自旋时间过长以及线程安全的范围不能灵活控制
死锁问题
发生必要条件
互斥条件
请求与保持条件
不剥夺条件
循环等待条件
定位死锁
jstack
ThreadMXBean
修复策略
避免策略
改变锁的获取顺序防止相反顺序获取锁这种情况的发生
检测与恢复策略
鸵鸟策略
AQS 框架
AbstractQueuedSynchronizer
AQS 在 ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch、ThreadPoolExcutor 的 Worker 中都有运用(JDK 1.8),AQS 是这些类的底层原理
状态、队列和期望协作工具类去实现的获取/释放等重要方法
消息中间件
RocketMQ
基本概念
消息模型(Message Model)
由 Producer、Broker、Consumer 三部分组成,其中Producer 负责生产消息,Consumer 负责消费消息,Broker 负责存储消息
消息生产者(Producer)
同步发送、异步发送、顺序发送、单向发送
消息消费者(Consumer)
拉取式消费、推动式消费
主题(Topic)
每条消息只能属于一个主题,是RocketMQ进行消息订阅的基本单位
代理服务器(Broker Server)
消息中转角色,负责存储消息、转发消息
名字服务(Name Server)
拉取式消费(Pull Consumer)
推动式消费(Push Consumer)
生产者组(Producer Group)
消费者组(Consumer Group)
集群消费(Clustering)
相同Consumer Group的每个Consumer实例平均分摊消息
广播消费(Broadcasting)
相同Consumer Group的每个Consumer实例都接收全量的消息
普通顺序消息(Normal Ordered Message)
严格顺序消息(Strictly Ordered Message)
消息(Message)
标签(Tag)
特性(features)
订阅与发布
消息顺序
消息过滤
消息可靠性
Broker非正常关闭
Broker异常Crash
OS Crash
机器掉电,但是能立即恢复供电情况
机器无法开机(可能是cpu、主板、内存等关键设备损坏)
磁盘设备损坏
Broker异常Crash
OS Crash
机器掉电,但是能立即恢复供电情况
机器无法开机(可能是cpu、主板、内存等关键设备损坏)
磁盘设备损坏
至少一次
回溯消费
事务消息
应用本地事务和发送消息操作可以被定义到全局事务中,要么同时成功,要么同时失败。RocketMQ的事务消息提供类似 X/Open XA 的分布事务功能,通过事务消息能达到分布式事务的最终一致
定时消息
消息重试
消息重投
流量控制
死信队列
死信队列用于处理无法被正常消费的消息。当一条消息初次消费失败,消息队列会自动进行消息重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列 不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。
架构原理
Broker
作用:存储转发消息。每个borker可以有自己的副本slave。
每隔30秒发送心跳到NameServer中
存储
所有topic都写入同一个文件中
为每一个消费者组存储消费topic最后一个offset单独存储(consume queue)
物理存储
commit log
comsume queue
index file
PageCache
零拷贝
内存映射(Memery Map)mmap
Topic
可以根据tag之类过滤消息
NameServer
Broker会注册到NameServer,Producer和Consumer用NameServer来发现Broker
每隔10秒检查Broker的最新心跳时间,如果超过120S都没有发送心跳,则从路由中移除
实现了AP,可用性(Availability)、分区容错性(Partition tolerance)
Zookeeper实现了CP,一致性(Consistency)、分区容错性(Partition tolerance)
Producer和Consumer每隔30秒拉取NameServer上的信息。ScheduleAtFixRate
Producer
每隔30秒拉取NameServer上路由信息
消息发送规则
SelectMessageQueueByHash(默认)自增轮询方式
SelectMessageQueueByRandom随机选择一个队列
SelectMessageQueueByMachineRoom 返回空
自定义实现MessageQueueSelector
顺序消息
生产者发送消息到达broker是有序,不能使用多线程发送,需要顺序发送
写入broker的时候顺序写入,相同主体集中写入,选择同一个queue,MessageQueueSelector传入相同的hashKey
消费者消费的时候只能有一个线程
事务消息
延迟消息
定时消息(延迟队列)是指消息发送到broker后,不会立即被消费,等待特定时间投递给真正的topic。 broker有配置项messageDelayLevel,默认值为“1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h”,18个level。可以配置自定义messageDelayLevel
定时消息会暂存在名为SCHEDULE_TOPIC_XXXX的topic中,并根据delayTimeLevel存入特定的queue,queueId = delayTimeLevel – 1,即一个queue只存相同延迟的消息,保证具有相同发送延迟的消息能够顺序消费。broker会调度地消费SCHEDULE_TOPIC_XXXX,将消息写入真实的topic
Consumer
消费方式
集群模式
广播模式
消费模式
PULL
通过长轮询在没有消息时Hold住请求
PUSH
注册MessageListener监听器
负载均衡
连续分配(默认)AllocateMessageQueueAveragely
轮流:AllocateMessageQueueAveragelyByCircle
通过配置:AllocateMessageQueueByConfig
一致性Hash:AllocateMessageQueueConsistentHash
指定一个broker的topic中的queue:AllocateMessageQueueByMachineRoom
按broker的机房就近:AllocateMachineRoomNearBy
消费者重试及死信队列
重试时间不对衰减,最多重试16次
MessageQueue
默认8个队列
高可用
主从同步
意义
数据备份
高可用
提高性能
消费实时
数据同步
集群名字相同,连接到相同NameServer,brokerId=0代表master,1代表slave
刷盘类型
主从异步复制
主从同步双写
异步刷盘
同步刷盘
主从同步流程
从服务建立TCP连接主服务器,每隔5S向主服务器发送commitLog文件最大偏移量拉取还未同步消息
主服务器开启监听端口,监听从服务器发送过来的消息,解析并返回查找出未同步的消息给从服务器
客户端收到主服务器的消息后,将这批消息写入commitLog文件中,然后更新commitLog拉取偏移量,介者继续向主服务器拉取未同步消息
故障转移
Dledger集群搭建
RabbitMQ
Kafka
Pulsar
分布式存储
Redis
基础
数据类型
Stirng
存储类型
用来存储INT,float,string
操作命令
获取指定范围字符getrange hello 0 1
获取长度 strlen hello
字符串追加内容 append hello world
设置多个值 mset hello 123 world 345
获取多个值 mget hello world
设置值,如果key存在则不成功 setnx hello 333
加过期时间 单独expire加过期 set key value [expiration Ex seconds|PX milliseconds][NX|XX]
使用参数的方式 set k1 v1 EX 10 NX
整数值递增值不存在会得到1 incr hello incrby hello 100
整数值递减 decr hello decr hello 100
浮点数增量 set mf 2.6 / incrbyfloat mf 7.3
存储原理
数据模型HashTable
dictEntry包含key value和下一个指针
Simple Dynamic String简单动态字符串存储
int 存储8个字节长整型
embstr代表embstr格式的SDS,存储小于44个字节的字符串
raw存储大于44个字节的字符串
原因:C语言没有String类型,只能用char[]实现
embstr分配一次内存空间(为RedisObject和SDS分配空间是连续的),raw需要分配两次内存空间(分别为RedisObject和SDS分配空间)
应用场景
缓存,分布式锁,分布式数据共享,全局ID,计数器,限流
Hash
存储类型
存储多个无序的键值对,只使用一个KEY
操作命令
hset/hmset/hget/hmget/hkeys/hvals/hgetall
hdel/hlen
存储原理
ziplist
hash对象保存的键值对小于512个,所有键值对的健和值的长度小于64byte
否则存储结构转换为hashtable
hashtable
场景
比String存储节省更多key的空间,也更加便于管理
List
存储类型
存储有序的字符串,元素可以重复
操作命令
lpush/rpush/lpop/rpop
lindex/lrange
存储原理
3.2版本之后统一采用quicklist存储,quicklist是数组+链表的格式
Set
存储类型
String类型无序集合
操作命令
sadd/smembers/scard/spop/srem
存储原理
元素是整数类型用intset存储
不是整数类型用hashtable存储
应用场景
抽奖,点赞,打卡,用户关注等
ZSet
存储类型
存储有序集合,每个元素有个score,按从小到大排名
操作命令
zadd/zcard
存储原理
默认使用ziplist存储
skiplist跳表
应用场景
排行榜
Hyperloglog
统计有个集合中不重复的元素个数
Geo
地理位置信息,计算地理位置距离
Streams
5.0版本,用于可持久化的消息队列
原理
发布订阅模式
channel
支持?和*占位符
redis事务
特性
redis操作时原子性的
按照进入队列的顺序执行
不会收到其它客户端请求影响
事务不能嵌套,多个multi命令效果一样
用法
muti开启事务
exec执行事务
discard取消事务
watch监视
lua脚本
eval执行lua脚本
redis快速的原因
纯内存结构
请求处理单线程
多路复用机制
内存回收
过期策略
立即过期(主动淘汰)
惰性过期(被动淘汰)
定期过期
淘汰策略
最大内存设置
LRU最近最少使用
LFU最不常用
random随机
持久化机制
RDB默认(如果开启了AOF,优先使用AOF)
自动触发
shutdown
flushall
手动触发
save
bigsave
AOF
采用日志的形式来记录写操作
分布式
redis主从复制
全量复制,增量复制
哨兵机制
raft算法
客户端sharding
一致性HASH
代理Proxy
Redis Cluster
redis常用客户端
jedis redisson luttuce
高并发
缓存雪崩
大量热点数据同时过期失效,请求落到数据库中
加互斥锁或者使用队列,只允许一个线程去查询数据库
缓存定时预先更新,避免同时失效
加随机值,使key在不同时间失效
缓存永不过期
缓存穿透
Hash碰撞
布隆过滤器
判断不存在则一定不存在,判断存在不一定存在
Guava BF实现
缓存击穿
ShardingSphere
概念
分库分表
垂直分表
垂直分表的处理方式就是将一个表按照字段分成多张表,每个表存储其中一部分字段
水平分表
水平分库是把同一个表的数据按一定规则拆分到不同的数据库中,每个库同样可以位于不同的服务器上
规则算法
取模算法
范围限定算法
预定义算法
读写分离
读写分离,主要解决的就是高并发下的数据库访问,也是一种常用的解决方案
核心产品
Sharding-JDBC、Sharding-Proxy 及 Sharding-Sidecar
实现方式
理解 JDBC 规范以及 ShardingSphere 对 JDBC 规范的重写方式,是正确使用 ShardingSphere 实现数据分片的前提
JDBC规范
JDBC(Java Database Connectivity)的设计初衷是提供一套用于各种数据库的统一标准
JDBC API 是我们访问数据库的主要途径,也是 ShardingSphere 重写 JDBC 规范并添加分片功能的入口
核心配置
行表达式
ShardingRuleConfiguration
TableRuleConfiguration
ShardingStrategyConfiguration
YamlShardingRuleConfiguration
YamlEngine 和 YamlSwapper
核心功能
数据分片
读写分离
分布式事务
数据脱敏
编排治理
MongoDB
FastDFS
Elasticsearch
分布式调度任务
xxl-job
quartz
Elastic-job
分布式协调
Zookeeper
基础
数据模型
znode 节点类型与特性
持久节点
要显式调用 delete 函数进行删除操作
临时节点
客户端会话因超时或发生异常而关闭时,节点也相应在 服务器上被删除。
要显式调用 delete 函数进行删除操作
要显式调用 delete 函数进行删除操作
有序节点
有序节点是在持久节点和临时节点特性的基础上,增加了一个节点有序的性质
节点结构
czxid
表示该节点被创建时事务ID
mzxid
表示该节点最后一次被更新时的事务ID
pzxid
表示该节点的子节点列表最后一次被修改时的事务ID
ctime
表示该节点的创建时间
mtime
表示该节点最后一次被更新时间
version
数据节点的版本号
cversion
子节点的版本号
aversion
节点ACL版本号
ephemeralOwner
创建该临时节点会话SessionId,如果是持久节点为0
dataLength
数据内容长度
numChildren
当前节点的子节点个数
发布订阅模式
Watch 监控机制
客户端
标记该会话是一个带有 Watch 事件的请求
将 Watch 事件存储到 ZKWatchManager
服务端
解析收到的请求是否带有 Watch 注册事件
将对应的 Watch 事件存储到 WatchManager
ACL 权限控制
权限模式(Scheme)
范围验证
口令验证
授权对象(ID)
权限信息(Permission)
数据节点(create)创建权限,授予权限的对象可以在数据节点下创建子节点;
数据节点(wirte)更新权限,授予权限的对象可以更新该数据节点;
数据节点(read)读取权限,授予权限的对象可以读取该节点的内容以及子节点的信息;
数据节点(delete)删除权限,授予权限的对象可以删除该数据节点的子节点;
数据节点(admin)管理者权限,授予权限的对象可以对该数据节点体进行 ACL 权限设置
序列化
Jute 的序列解决方案
Binary、Csv、XML 三种方式的序列化操作
Watch 监控
ZooKeeper 集群模式
Leader 角色服务器
负责管理集群中其他的服务器,是集群中工作的分配和调度者。
Follow 服务器
主要工作是选举出 Leader 服务器,在发生 Leader 服务器选举的时候,系统会从 Follow 服务器之间根据多数投票原则,选举出一个 Follow 服务器作为新的 Leader 服务器。
Observer 服务器
主要负责处理来自客户端的获取数据等请求,并不参与 Leader 服务器的选举操作,也不会作为候选者被选举为 Leader 服务器。
客户端连接会话
会话的创建
会话 ID
会话超时时间
会话关闭状态
会话管理策略
分桶策略
Curator
会话状态
CONNECTED(已连接状态):当客户端发起的会话成功连接到服务端后,该条会话的状态变为 CONNECTED 已连接状态。
READONLY(只读状态):当一个客户端会话调用
CuratorFrameworkFactory.Builder.canBeReadOnly() 的时候,该会话会一直处于只读模式,直到重新设置该条会话的状态类型。
CuratorFrameworkFactory.Builder.canBeReadOnly() 的时候,该会话会一直处于只读模式,直到重新设置该条会话的状态类型。
SUSPENDED(会话连接挂起状态):当进行 Leader 选举和 lock 锁等操作时,需要先挂起客户端的连接。注意这里的会话挂起并不等于关闭会话,也不会触发诸如删除临时节点等操作。
RECONNECTED(重新连接状态):当已经与服务端成功连接的客户端断开后,尝试再次连接服务端后,该条会话的状态为 RECONNECTED,也就是重新连接。重新连接的会话会作为一条新会话在服务端运行,之前的临时节点等信息不会被保留。
LOST(会话丢失状态):当客户端与服务器端因为异常或超时,导致会话关闭时,该条会话的状态就变为 LOST。
Leader 选举
数据的一致性
Leader发起事务性请求投票
投票通过
执行事务性请求
进行数据一致性操作
广播模式
恢复模式
服务启动时的 Leader 选举
发起投票
每次投票时,发送的服务器的 myid(服务器标识符)和 ZXID (集群投票信息标识符)等选票信息字段都指向本机服务器
接收投票
统计投票
对比的内容是 ZXID,ZXID 数值比较大的投票信息优先作为 Leader 服务器。
如果每个投票信息中的 ZXID 相同,就会接着比对投票信息中的 myid 信息字段,选举出 myid 较大的服务器作为 Leader 服务器
如果每个投票信息中的 ZXID 相同,就会接着比对投票信息中的 myid 信息字段,选举出 myid 较大的服务器作为 Leader 服务器
Leader 与 Follower 的数据同步策略
同步条件
存在Leader 服务器
同步过程
DIFF 同步即差异化同步的方式
TRUNC+DIFF 同步代表先回滚再执行差异化的同步
TRUNC 同步是指仅回滚操作
SNAP 同步的意思是全量同步
同步后的处理
leader作用
事务的请求处理与调度分析
Follow作用
非事务请求的处理与 Leader 的选举分析
Observer
Observer 不参与 Leader 服务器的选举工作,也不会被选举为 Leader 服务器
数据文件
内存数据、事务日志、数据快照
数据快照是每间隔一段时间才把内存数据存储到本地磁盘,因此数据并不会一直与内存数据保持一致
应用
分布式锁
分布式死锁
超时方式
在创建分布式线程的时候,对每个线程都设置一个超时时间
死锁检测
把死锁检测理解为一个运行在各个服务器系统上的线程或方法,该方法专门用来探索发现应用服务上的线程是否发生了死锁
排他锁
利用 ZooKeeper 数据模型的临时顺序节点和 Watch 监控机制,在客户端通过创建数据节点的方式来获取锁,通过删除数据节点来释放锁
共享锁
分布式 ID 生成器
生成策略
UUID 方式
数据库序列方式
TDDL 中的序列化实现
snowflake 算法
唯一性、安全性、递增性以及扩展性
实现负载均衡服务器
常用算法
负载均衡算法
轮询法
随机法
原地址哈希法
加权轮询法
加权随机法
最小连接数法
DUBBO
算法
二阶段提交
三阶段提交
ZAB协议算法
崩溃恢复
原子广播协议
Paxos算法
提议者(Proposer):提出提案(Proposal)。Proposal 信息包括提案编号(Proposal ID)和提议的值(Value)。
决策者(Acceptor):参与决策,回应 Proposers 的提案。收到 Proposal 后可以接受提案,若 Proposal 获得超过半数 Acceptors 的许可,则称该 Proposal 被批准
决策学习者:不参与决策,从 Proposers/Acceptors 学习最新达成一致的提案(Value)
子主题
Raft 算法
网络通信
序列化与反序列化
RPC通信技术
WebService
RMI
Hession
Thrift
Dubbo
Netty
基础知识
IO
阻塞(Block)
往往需要等待缓冲区中的数据准备好过后才处理其他的事情,否则一直等待在那里
非阻塞(Non-Block)
当我们的进程访问我们的数据缓冲区的时候,如果数据没有准备好则直接返回,不会等待。如果数据已经
准备好,也直接返回。
准备好,也直接返回。
同步(Synchronization)
应用程序要直接参与IO 读写的操作
异步(Asynchronous)
所有的IO 读写交给操作系统去处理,应用程序只需要等待通知
5 种主要 I/O 模式
BIO
BIO 是面向流的,阻塞IO(多线程)
NIO
NIO面向缓冲区,非阻塞IO(反应堆Rector),选择器(轮询机制)
缓冲区(Buffer)
通道(Channel)
选择器(Selector)
I/O 多路复用
select、poll、epoll
信号驱动 I/O
异步 I/O
AIO
NIO 升级版本,提供了异步非阻塞的 IO 操作方式
netty的 I/O 模型
基于非阻塞 I/O 实现
底层依赖 JDK NIO 框架的多路复用器 Selector
一个多路复用器 Selector 可以同时轮询多个 Channel
采用 epoll 模式后,只需要一个线程负责 Selector 的轮询,就可以接入成千上万的客户端
事件分发器有两种设计模式:Reactor 和 Proactor,Reactor 采用同步 I/O, Proactor 采用异步 I/O
netty优势
易用性
稳定性
可扩展性
更低的资源消耗
对象池复用技术
零拷贝技术
使用netty产品
服务治理:Apache Dubbo、gRPC
大数据:Hbase、Spark、Flink、Storm
搜索引擎:Elasticsearch
消息队列:RocketMQ、ActiveMQ
架构脉络
Netty 整体结构
Core 核心层
Protocol Support 协议支持层
Transport Service 传输服务层
Netty 逻辑架构
网络通信层
BootStrap、ServerBootStrap、Channel
事件调度层
EventLoopGroup、EventLoop
服务编排层
ChannelPipeline、ChannelHandler、ChannelHandlerContext
BootStrap
服务端启动过程
配置线程池
Netty Reactor 线程模型的具体实现方式
单线程模型:EventLoopGroup 只包含一个 EventLoop,Boss 和 Worker 使用同一个EventLoopGroup
多线程模型:EventLoopGroup 包含多个 EventLoop,Boss 和 Worker 使用同一个EventLoopGroup
主从多线程模型:EventLoopGroup 包含多个 EventLoop,Boss 是主 Reactor,Worker 是从 Reactor,它们分别使用不同的 EventLoopGroup,主 Reactor 负责新的网络连接 Channel 创建,然后把 Channel 注册到从 Reactor。
Channel 初始化
设置 Channel 类型
NioServerSocketChannel 异步 TCP 服务端
NioSocketChannel 异步 TCP 客户端
OioServerSocketChannel 同步 TCP 服务端
OioSocketChannel 同步 TCP 客户端
NioDatagramChannel 异步 UDP 连接
OioDatagramChannel 同步 UDP 连接
NioSocketChannel 异步 TCP 客户端
OioServerSocketChannel 同步 TCP 服务端
OioSocketChannel 同步 TCP 客户端
NioDatagramChannel 异步 UDP 连接
OioDatagramChannel 同步 UDP 连接
端口绑定
EventLoop
主从多线程模型
连接注册
Channel 建立后,注册至 Reactor 线程中的 Selector 选择器
事件轮询
轮询 Selector 选择器中已注册的所有 Channel 的 I/O 事件
事件分发
为准备就绪的 I/O 事件分配相应的处理线程
任务处理
Reactor 线程还负责任务队列中的非 I/O 任务,每个 Worker 线程从各自维护的任务队列中取出任务异步执行
事件处理机制
事件等待和处理的程序模型,可以解决多线程资源消耗高的问题
事件执行的方式通常分为立即执行、延后执行、定期执行
epoll 空轮询的 Bug
每次执行 Select 操作之前记录当前时间 currentTimeNanos
time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos,如果事件轮询的持续时间大于等于 timeoutMillis,那么说明是正常的,否则表明阻塞时间并未达到预期,可能触发了空轮询的 Bug
Netty 引入了计数变量 selectCnt。在正常情况下,selectCnt 会重置,否则会对 selectCnt 自增计数。当 selectCnt 达到 SELECTOR_AUTO_REBUILD_THRESHOLD(默认512) 阈值时,会触发重建 Selector 对象
任务处理机制
普通任务:通过 NioEventLoop 的 execute() 方法向任务队列 taskQueue 中添加任务
定时任务:通过调用 NioEventLoop 的 schedule() 方法向定时任务队列 scheduledTaskQueue 添加一个定时任务,用于周期性执行该任务
尾部队列:tailTasks 相比于普通任务队列优先级较低,在每次执行完 taskQueue 中任务后会去获取尾部队列中任务执行
Pipeline
ChannelPipeline
内部结构
负责调度各种类型的 ChannelHandler
每个 Channel 会绑定一个 ChannelPipeline
每个 ChannelPipeline 包含多个 ChannelHandlerContext
所有 ChannelHandlerContext 之间组成了双向链表
每个 ChannelHandler 都对应一个 ChannelHandlerContext,所以实际上 ChannelPipeline 维护的是它与 ChannelHandlerContext 的关系
ChannelHandler
接口设计
ChannelInboundHandler
ChannelOutboundHandler
Inbound 事件和 Outbound 事件的传播方向相反,Inbound 事件的传播方向为 Head -> Tail,而 Outbound 事件传播方向是 Tail -> Head
粘包/拆包
定义应用层的通信协议
消息长度固定
特定分隔符
消息长度 + 消息内容
魔数 2byte | 协议版本号 1byte | 序列化算法 1byte | 报文类型 1byte | 状态 1byte |保留字段 4byte|数据长度 4byte| 数据内容(长度不定)
Netty 常用编码器类型
MessageToByteEncoder 对象编码成字节流
MessageToMessageEncoder 一种消息类型编码成另外一种消息类型
Netty 常用解码器类型
ByteToMessageDecoder/ReplayingDecoder 将字节流解码为消息对象
MessageToMessageDecoder 将一种消息类型解码为另外一种消息类型
常用的解码器
固定长度解码器 FixedLengthFrameDecoder
特殊分隔符解码器 DelimiterBasedFrameDecoder
长度域解码器 LengthFieldBasedFrameDecoder
堆外内存
ByteBuffer#allocateDirect
DirectByteBuffer
Unsafe#allocateMemory
分配的内存必须自己手动释放
ByteBuf
相比于 ByteBuffer的优势
容量可以按需动态扩展,类似于 StringBuffer;
读写采用了不同的指针,读写模式可以随意切换,不需要调用 flip 方法
通过内置的复合缓冲类型可以实现零拷贝
支持引用计数
支持缓存池
内部结构
废弃字节
可读字节
可写字节
可扩容字节
内存分配器 jemalloc
Netty零拷贝技术
堆外内存,避免 JVM 堆内存到堆外内存的数据拷贝
CompositeByteBuf 类可以组合多个 Buffer 对象合并成一个逻辑上的对象,避免通过传统内存拷贝的方式将几个 Buffer 合并成一个大的 Buffer
通过 Unpooled.wrappedBuffer 可以将 byte 数组包装成 ByteBuf 对象,包装过程中不会产生内存拷贝。
ByteBuf.slice 操作与 Unpooled.wrappedBuffer 相反,slice 操作可以将一个 ByteBuf 对象切分成多个 ByteBuf 对象,切分过程中不会产生内存拷贝,底层共享一个 byte 数组的存储空间
Netty 使用 FileRegion 实现文件传输,FileRegion 底层封装了 FileChannel#transferTo() 方法,可以将文件缓冲区的数据直接传输到目标 Channel,避免内核缓冲区和用户态缓冲区之间的数据拷贝,这属于操作系统级别的零拷贝
Nginx应用
应用场景
代理服务
反向代理(proxy_pass)
负载均衡
七层(upstream)
round robin
ip hash
url hash
weight
fair(第三方)
四层(Nginx PLUS)
缓存
proxy_cache
HTTPS
动静分离
静态资源(动态资源)
跨域访问
防盗链
动态资源(tomcat,php-fpm)
限流
算法
令牌桶
漏桶
实现
limit_conn_zone
limit_req_zone
ngx_http_upstream_module
分布式微服务
SpringBoot
Spring Cloud Netflix
注册中心Eureka
配置
@EnableEurekaServer
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
客户端
@EnableDiscoveryClient
@LoadBalanced
restTemplate
服务提供者
注册
同步
续约
服务消费者
获取服务
服务调用
Region
Zone
服务下线
服务注册中心
失效剔除
自我保护
配置详解
通信协议
Jersey
XStream
客户端负载均衡Ribbon
RestTemplate
GET
getForObject
getForEntity
POST
postForEntity
postForObject
postForLocation
PUT
DELETE
策略
随机
轮询
重试
权重
ClientConfigEnabledRoundRobinRule
BestAvailableRule
PredicateBasedRule
AvailablityFilteringRule
ZoneAvoidanceRule
服务容错保护Hystrix
功能
服务降级
服务熔断
线程和信号隔离
请求缓存
请求合并
服务监控
代码
@EnableCircuitBreaker
@HystrixCommand
原理
Command原理
缓存结果
断路器状态
线程池/请求队列/信号量是否满了
健康指标
断路器原理
依赖隔离
使用详解
创建请求命令
定义服务降级
异常处理
请求缓存
@CacheKey
@CacheResult
@CacheRemove
请求合并
配置详解
仪表盘
@EnableHystrixDashboard
turbine集群监控
与消息代理结合
声明式服务调用Feign
功能
封装前两个
@EnableFeignClients
@FeignClient(name="HELLO-SERVICE", fallback = HelloServiceFallback.class)
参数绑定
继承特性
服务端和客户端公用相同接口
注解相似
优缺点
不建议使用
配置
Ribbon配置
指定服务配置
全局配置
重试配置
Hystrix配置
全局配置
禁用配置
降级配置
请求压缩
日志设置
API网关服务Zuul
功能
请求路由
Url
面向服务路由
请求过滤
代码
@EnableZuulProxy
ZuulFilter
PatternServiceRouteMapper
配置
路径匹配
忽略表达式
路由前缀
本地跳转
Cookie和信息头
敏感字段
重定向问题
过滤器
过滤类型
filterType
pre
route
post
error
执行顺序
filterOrder
执行条件
shouldFilter
具体操作
run
禁用过滤器
动态加载
动态路由
@RefreshScope
动态过滤器
分布式配置中心Config
功能
代码
@RefreshScope
@EnableConfigServer
服务端详解
仓库
git
svn
本地仓库
本地文件
健康监测
属性覆盖
安全保护
Spring Security
加密解密
高可用
服务化
客户端详解
URI指定配置中心
失败快速响应与重试
获取远程配置
动态刷新配置
消息总线Bus
消息代理
Message Broker
通信调度
最小化应用间的依赖
刷新服务的配置
消息驱动的微服务Stream
SpringBoot+SpringIntegration
Gateway 统一网关
服务动态路由
服务统一限流熔断
服务统一缓存
服务统一授权认证
服务统一性能监控
服务统一灰度发布
Spring Cloud Alibaba
Doubbo
特性
面向接口代理的高性能RPC调用
智能负载均衡
服务自动注册与发现
高度可扩展能力
运行期流量调度
架构
服务容器负责启动,加载,运行服务提供者。
服务提供者在启动时,向注册中心注册自己提供的服务
服务消费者在启动时,向注册中心订阅自己所需的服务。
注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
Registry:注册中心
Provider:服务提供者
Consumer:服务消费者
Monitor:监控中心
Container:服务运行容器
基础知识
dubbo-demo
基于xml配置
基于注解配置
基于 API 配置
URL配置总线
protocol://username:password@host:port/path?key=value&key=value
protocol:URL 的协议。我们常见的就是 HTTP 协议和 HTTPS 协议,当然,还有其他协议,如 FTP 协议、SMTP 协议等
username/password:用户名/密码。 HTTP Basic Authentication 中多会使用在 URL 的协议之后直接携带用户名和密码的方式
host/port:主机/端口。在实践中一般会使用域名,而不是使用具体的 host 和 port
path:请求的路径。
parameters:参数键值对
Dubbo SPI
JDK SPI
在 Classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,此文件记录了该 jar 包提供的服务接口的具体实现类
Dubbo SPI
META-INF/services/ 目录:该目录下的 SPI 配置文件用来兼容 JDK SPI
META-INF/dubbo/ 目录:该目录用于存放用户自定义 SPI 配置文件
META-INF/dubbo/internal/ 目录:该目录用于存放 Dubbo 内部使用的 SPI 配置文件
META-INF/dubbo/ 目录:该目录用于存放用户自定义 SPI 配置文件
META-INF/dubbo/internal/ 目录:该目录用于存放 Dubbo 内部使用的 SPI 配置文件
SPI 配置文件改成了 KV 格式
@SPI 注解
时间轮定时任务
失败重试
Provider 向注册中心进行注册失败时的重试操作,或是 Consumer 向注册中心订阅时的失败重试等
周期性定时任务
定期发送心跳请求,请求超时的处理,或是网络连接断开后的重连机制
zookeeper
Client 节点
Leader 节点
Follower 节点
Observer 节点
ZNode 节点类型
持久节点
持久顺序节点
临时节点
临时顺序节点
代理模式
JDK 动态代理
CGLib
是一个基于 ASM 的字节码生成库
CGLib 与 JDK 动态代理之间可以相互补充:在目标类实现接口时,使用 JDK 动态代理创建代理对象;当目标类没有实现接口时,使用 CGLib 实现动态代理的功能
Javassist
开源的生成 Java 字节码的类库
netty
Netty I/O 模型设计
传统阻塞 I/O 模型
I/O 多路复用模型
Netty 线程模型设计
单 Reactor 单线程
单 Reactor 多线程
主从 Reactor 多线程
基本用法
启动时检查
缺省会在启动时检查依赖的服务是否可用,不可用时会抛出异常,阻止 Spring 初始化完成,以便上线时,能及早发现问题,默认 check="true"
集群容错
Failover Cluster(默认)
失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries="2" 来设置重试次数(不含第一次)
Failfast Cluster
快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录
Failsafe Cluster
失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作
Failback Cluster
失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作
Forking Cluster
并行调用多个服务器,只要一个成功即返回
Broadcast Cluster
广播调用所有提供者,逐个调用,任意一台报错则报错
负载均衡
Random LoadBalance(默认)
随机,按权重设置随机概率
有一组服务器 servers = [A, B, C],他们对应的权重为 weights = [5, 3,
2],权重总和为10。现在把这些权重值平铺在一维坐标值上,[0, 5) 区间属于服务器 A,[5, 8) 区间属于
服务器 B,[8, 10) 区间属于服务器 C
2],权重总和为10。现在把这些权重值平铺在一维坐标值上,[0, 5) 区间属于服务器 A,[5, 8) 区间属于
服务器 B,[8, 10) 区间属于服务器 C
RoundRobin LoadBalance
轮询,按公约后的权重设置轮询比率
LeastActive LoadBalance
最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差
根据目标集群服务器列表,处理性能最高的,权重也越高。处理性能较低的,权重也比较低
根据请求处理的吞吐量 -> 发起一次请求(开始),计数器+1、 一次请求处理完成,计数器-1
根据请求处理的吞吐量 -> 发起一次请求(开始),计数器+1、 一次请求处理完成,计数器-1
ConsistentHash LoadBalance
一致性 Hash,相同参数的请求总是发到同一提供者
根据参数进行hash取模。 默认是根据 {第一个参数}。
多协议
dubbo
hessian
thrift
grpc http2.0 / protobuff
rest
rmi
泛化调用
实现一个通用的服务测试框架,可通过 GenericService 调用所有服务实现
隐式参数
通过 RpcContext 上的 setAttachment 和 getAttachment 在服务消费方和提供方之间进行参数的隐式传递
服务降级
主机绑定
查找环境变量中是否存在启动参数 [DUBBO_IP_TO_BIND] =服务注册的ip
读取配置文件, dubbo.protocols.dubbo.host= 服务注册的ip
InetAddress.getLocalHost().getHostAddress() 获得本机ip地址
通过Socket去连接注册中心,从而获取本机IP
会轮询本机的网卡,直到找到合适的IP地址
上面获取到的ip地址是bindip,如果需要作为服务注册中心的ip, DUBBO_IP_TO_REGISTRY -
dDUBBO_IP_TO_REGISTRY=ip
读取配置文件, dubbo.protocols.dubbo.host= 服务注册的ip
InetAddress.getLocalHost().getHostAddress() 获得本机ip地址
通过Socket去连接注册中心,从而获取本机IP
会轮询本机的网卡,直到找到合适的IP地址
上面获取到的ip地址是bindip,如果需要作为服务注册中心的ip, DUBBO_IP_TO_REGISTRY -
dDUBBO_IP_TO_REGISTRY=ip
配置的优先级问题
方法层面的配置要优先于接口层面的配置, 接口层面的配置要优先于全局配置
如果级别一样,以客户端的配置优先,服务端次之
如果级别一样,以客户端的配置优先,服务端次之
整体设计
SPI扩展
java SPI
SpringFactoriesLoader
Dubbo SPI
Sentinel 限流降级熔断
限流源码剖析
限流类型详解及源码剖析
QPS限流源码剖析
线程数限流源码剖析
限流模式详解及源码剖析
限流效果详解及源码剖析
请求快速失败
请求预热
请求排队
限流算法详解及源码剖析
计数器限流
滑动时间窗口限流源码剖析
令牌桶限流源码剖析
漏桶限流源码剖析
熔断降级源码剖析
服务断路器设计思想及源码剖析
接口平均相应时间超时熔断源码剖析
接口异常比例过高熔断源码剖析
接口异常数过多熔断源码剖析
服务降级注解自动化配置源码剖析
热点限流规则源码剖析
秒杀场景指定热点参数限流实现
系统负载限流源码剖析
系统级负载Load限流
系统级平均响应时间限流
系统级线程数限流
系统级QPS限流
系统CPU使用率限流
系统黑白名单授权规则限流
Nacos 分布式配置中心详解
高可用分布式配置中心实战
多环境切换及配置共享
运行时配置动态刷新及服务热加载
Seata 微服务分布式事务详解及源码分析
Seata全局事务注册源码剖析
Seata分支事务客户端注册源码剖析
Seata分支事务客户端全局锁冲突自旋设计原理剖析
Seata分支事务服务端全局锁设计源码剖析
Seata全局事务提交源码剖析
Seata全局事务回滚源码剖析
Seata分支事务第二阶段异步提交源码剖析
Seata分支事务第二阶段生成反向Sql执行回滚源码剖析
微服务的用户认证与授权详解
微服务API安全机制详解
微服务安全之Oauth2协议详解
微服务安全之传统Session的认证与授权
微服务安全之Token机制的认证与授权
JWT安全认证方案详解
Dubbo
特性
面向接口代理的高性能RPC调用
智能负载均衡
服务自动注册与发现
高度可扩展能力
运行期流量调度
架构
服务容器负责启动,加载,运行服务提供者。
服务提供者在启动时,向注册中心注册自己提供的服务
服务消费者在启动时,向注册中心订阅自己所需的服务。
注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
Registry:注册中心
Provider:服务提供者
Consumer:服务消费者
Monitor:监控中心
Container:服务运行容器
基础知识
dubbo-demo
基于xml配置
基于注解配置
基于 API 配置
URL配置总线
protocol://username:password@host:port/path?key=value&key=value
protocol:URL 的协议。我们常见的就是 HTTP 协议和 HTTPS 协议,当然,还有其他协议,如 FTP 协议、SMTP 协议等
username/password:用户名/密码。 HTTP Basic Authentication 中多会使用在 URL 的协议之后直接携带用户名和密码的方式
host/port:主机/端口。在实践中一般会使用域名,而不是使用具体的 host 和 port
path:请求的路径。
parameters:参数键值对
Dubbo SPI
JDK SPI
在 Classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,此文件记录了该 jar 包提供的服务接口的具体实现类
Dubbo SPI
META-INF/services/ 目录:该目录下的 SPI 配置文件用来兼容 JDK SPI
META-INF/dubbo/ 目录:该目录用于存放用户自定义 SPI 配置文件
META-INF/dubbo/internal/ 目录:该目录用于存放 Dubbo 内部使用的 SPI 配置文件
META-INF/dubbo/ 目录:该目录用于存放用户自定义 SPI 配置文件
META-INF/dubbo/internal/ 目录:该目录用于存放 Dubbo 内部使用的 SPI 配置文件
SPI 配置文件改成了 KV 格式
@SPI 注解
时间轮定时任务
失败重试
Provider 向注册中心进行注册失败时的重试操作,或是 Consumer 向注册中心订阅时的失败重试等
周期性定时任务
定期发送心跳请求,请求超时的处理,或是网络连接断开后的重连机制
zookeeper
Client 节点
Leader 节点
Follower 节点
Observer 节点
ZNode 节点类型
持久节点
持久顺序节点
临时节点
临时顺序节点
代理模式
JDK 动态代理
CGLib
是一个基于 ASM 的字节码生成库
CGLib 与 JDK 动态代理之间可以相互补充:在目标类实现接口时,使用 JDK 动态代理创建代理对象;当目标类没有实现接口时,使用 CGLib 实现动态代理的功能
Javassist
开源的生成 Java 字节码的类库
netty
Netty I/O 模型设计
传统阻塞 I/O 模型
I/O 多路复用模型
Netty 线程模型设计
单 Reactor 单线程
单 Reactor 多线程
主从 Reactor 多线程
基本用法
启动时检查
缺省会在启动时检查依赖的服务是否可用,不可用时会抛出异常,阻止 Spring 初始化完成,以便上线时,能及早发现问题,默认 check="true"
集群容错
Failover Cluster(默认)
失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries="2" 来设置重试次数(不含第一次)
Failfast Cluster
快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录
Failsafe Cluster
失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作
Failback Cluster
失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作
Forking Cluster
并行调用多个服务器,只要一个成功即返回
Broadcast Cluster
广播调用所有提供者,逐个调用,任意一台报错则报错
负载均衡
Random LoadBalance(默认)
随机,按权重设置随机概率
有一组服务器 servers = [A, B, C],他们对应的权重为 weights = [5, 3,
2],权重总和为10。现在把这些权重值平铺在一维坐标值上,[0, 5) 区间属于服务器 A,[5, 8) 区间属于
服务器 B,[8, 10) 区间属于服务器 C
2],权重总和为10。现在把这些权重值平铺在一维坐标值上,[0, 5) 区间属于服务器 A,[5, 8) 区间属于
服务器 B,[8, 10) 区间属于服务器 C
RoundRobin LoadBalance
轮询,按公约后的权重设置轮询比率
LeastActive LoadBalance
最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差
根据目标集群服务器列表,处理性能最高的,权重也越高。处理性能较低的,权重也比较低
根据请求处理的吞吐量 -> 发起一次请求(开始),计数器+1、 一次请求处理完成,计数器-1
根据请求处理的吞吐量 -> 发起一次请求(开始),计数器+1、 一次请求处理完成,计数器-1
ConsistentHash LoadBalance
一致性 Hash,相同参数的请求总是发到同一提供者
根据参数进行hash取模。 默认是根据 {第一个参数}。
多协议
dubbo
hessian
thrift
grpc http2.0 / protobuff
rest
rmi
泛化调用
实现一个通用的服务测试框架,可通过 GenericService 调用所有服务实现
隐式参数
通过 RpcContext 上的 setAttachment 和 getAttachment 在服务消费方和提供方之间进行参数的隐式传递
服务降级
主机绑定
查找环境变量中是否存在启动参数 [DUBBO_IP_TO_BIND] =服务注册的ip
读取配置文件, dubbo.protocols.dubbo.host= 服务注册的ip
InetAddress.getLocalHost().getHostAddress() 获得本机ip地址
通过Socket去连接注册中心,从而获取本机IP
会轮询本机的网卡,直到找到合适的IP地址
上面获取到的ip地址是bindip,如果需要作为服务注册中心的ip, DUBBO_IP_TO_REGISTRY -
dDUBBO_IP_TO_REGISTRY=ip
读取配置文件, dubbo.protocols.dubbo.host= 服务注册的ip
InetAddress.getLocalHost().getHostAddress() 获得本机ip地址
通过Socket去连接注册中心,从而获取本机IP
会轮询本机的网卡,直到找到合适的IP地址
上面获取到的ip地址是bindip,如果需要作为服务注册中心的ip, DUBBO_IP_TO_REGISTRY -
dDUBBO_IP_TO_REGISTRY=ip
配置的优先级问题
方法层面的配置要优先于接口层面的配置, 接口层面的配置要优先于全局配置
如果级别一样,以客户端的配置优先,服务端次之
如果级别一样,以客户端的配置优先,服务端次之
整体设计
SPI扩展
java SPI
SpringFactoriesLoader
Dubbo SPI
分布式日志监控 ELK
性能优化
JVM
JVM加载
加载
使用到加载.class文件
类加载器
启动类加载器 Bootstrap ClassLoader
扩展类加载器 Extension ClassLoader
应用程序类加载器 Application ClassLoader
自定义类加载器
双亲委派机制
验证
根据JVM虚拟机规范进行验证是否符合
准备
类分配一定的内存空间,类变量分配内存空间,分配默认初始值
解析
符合引用替换为实际引用
初始化
执行类的初始化代码
使用
卸载
内存区域
元数据空间(方法区)
存放类信息,常量池
程序计数器
记录执行的字节码指令位置
java虚拟机栈
栈帧
局部变量表 、操作数栈、动态链接、方法出口等
本地方法栈
堆内存
分代模型
年轻代
老年代
永久代
参数
-Xms 堆内存的最小大小
-Xmx 堆内存的最大大小
-Xmn 堆内存中的新生代大小,扣除新生代剩下的就是老年代的内存大小了
-XX:PermSize:永久代大小
-XX:MaxPermSize:永久代最大大小
-Xss:每个线程的栈内存大小
-XX:MetaspaceSize和-XX:MaxMetaspaceSize (jdk1.8)
-XX:CMSInitiatingOccupancyFaction 参数可以用来设置老年代占用多少比例的时候触发CMS垃圾回收,JDK 1.6里面默认的值是92%。
-XX:+UseCMSCompactAtFullCollection Full GC之后要再次进行“Stop the World”,停止工作线程,然后进行碎片整理,就是把存活对象挪到一起,空出来大片连续内存空间,避免内存碎片。
-XX:SurvivorRatio=8(新生代分区比例 8:2)
-XX:+UseConcMarkSweepGC(指定使用的垃圾收集器,这里使用CMS收集器)
-XX:+PrintGCDetails(打印详细的GC日志)
-XX:+UseConcMarkSweepGC(指定使用的垃圾收集器,这里使用CMS收集器)
-XX:+PrintGCDetails(打印详细的GC日志)
-XX:MaxTenuringThreshold 多少岁进入老年代,可以通过JVM参数“”来设置,默认是15岁
-XX:PretenureSizeThreshold”,直接进入老年代可以把他的值设置为字节数,比如“1048576”字节,就是1MB
-XX:CMSFullGCsBeforeCompaction”,这个意思是执行多少次Full GC之后再执行一次内存碎片整理的工作,默认是0
垃圾回收算法
复制算法
1个Eden区,2个Survivor区
老年代标记整理算法
Serial和Serial Old垃圾回收器:分别用来回收新生代和老年代的垃圾对象
ParNew和CMS垃圾回收器:ParNew现在一般都是用在新生代的垃圾回收器,CMS是用在老年代的垃圾回收器
G1垃圾回收器:统一收集新生代 和老年代
CMS
初始标记(stop the world)
并发标记
重新标记(stop the world)
并发清理
G1垃圾回收器
Java堆内存拆分为多个大小相等的Region
G1可以做到让你来设定垃圾回收对系统的影响,他自己通过把内存拆分为大量小Region,以及追踪每个Region中可以回收的对象大小和预估时间,最后在垃圾回收的时候,尽量把垃圾回收对系统造成的影响控制在你指定的时间范围内,同时在有限的时间内尽量回收尽可能多的垃圾对象
JVM最多可以有2048个Region,然后Region的大小必须是2的倍数,比如说1MB、2MB、4MB之类的
选择最小回收时间和最多回收对象的region
-XX:G1HeapRegionSize
-XX:G1NewSizePercent
-XX:G1MaxNewSizePercent
-XX:MaxGCPauseMills
比如年龄为1岁,2岁,3岁,4岁的对象的大小总和超过了Survivor的50%,此时4岁以上的对象全部会进入老年代,这就是动态年龄判定规则
Xms4096M -Xmx4096M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M
jstat
jstat -gccapacity PID:堆内存分析
jstat -gcnew PID:年轻代GC分析,这里的TT和MTT可以看到对象在年轻代存活的年龄和存活的最大年龄
jstat -gcnewcapacity PID:年轻代内存分析
jstat -gcold PID:老年代GC分析
jstat -gcoldcapacity PID:老年代内存分析
jstat -gcmetacapacity PID:元数据区内存分析
jstat -gc 11387 1000 10
jmap
使用jmap了解系统运行时的内存区域
jmap -heap PID
S0C 年轻代中第一个 survivor 的容量
S1C 年轻代中第二个 survivor 的容量
S0U 年轻代中第一个 survivor 目前已使用空间
S1U 年轻代中第二个 survivor 目前已使用空间
EC 年轻代中 Eden 的容量
EU 年轻代中 Eden 目前已使用空间
OC 老年代的容量
OU 老年代目前已使用空间
MC 元空间 metaspace 的容量
MU 元空间 metaspace 目前已使用空间
YGC 从应用程序启动到采样时 年轻代 中 gc 次数
YGCT 从应用程序启动到采样时 年轻代 中 gc 所用时间
FGC 从应用程序启动到采样时 老年代 中 gc 次数
FGCT 从应用程序启动到采样时 老年代 中 gc 所用时间
GCT 从应用程序启动到采样时 gc 用的 总时间
CCSC 压缩类空间大小
CCSU 压缩类空间 使用 大小
https://www.cnblogs.com/ostenant/p/9696226.html
jmap -histo PID
jmap -dump:live,format=b,file=dump.hprof PID
优化思路
尽量让每次Young GC后的存活对象小于Survivor区域的50%,都留存在年轻代里。尽量别让对象进入老年代。尽量减少Full GC的频率,避免频繁Full GC对JVM性能的影响。
-XX:NewSize=104857600 -XX:MaxNewSize=104857600 -XX:InitialHeapSize=209715200 -XX:MaxHeapSize=209715200 -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -XX:PretenureSizeThreshold=3145728 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:gc.log
-XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=5
CMS垃圾回收器默认是采用标记-清理算法,所以是会造成大量的内存碎片的。
5次full gc之后进行压缩
JVm参数模板
-Xms4096M -Xmx4096M -Xmn3072M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFaction=92 -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0
-XX:+CMSParallelInitialMarkEnabled 这个参数会在CMS垃圾回收器的“初始标记”阶段开启多线程并发执行
初始标记阶段,是会进行Stop the World的,会导致系统停顿,所以这个阶段开启多线程并发之后,可以尽可能优化这个阶段的性能,减少Stop the World的时间
-XX:+CMSScavengeBeforeRemark”,这个参数会在CMS的重新标记阶段之前,先尽量执行一次Young GC
-XX:TraceClassLoading -XX:TraceClassUnloading 这两个参数,顾名思义,就是追踪类加载和类卸载的情况,他会通过日志打印出来JVM中加载了哪些类,卸载了哪些类。
实际上一旦这个参数设置为0之后,直接导致clock - timestamp <= freespace * SoftRefLRUPolicyMSPerMB这个公式的右半边是0,就导致所有的软引用对象,比如JVM生成的那些奇怪的Class对象,刚创建出来就可能被一次Young GC给带着立马回收掉一些。
-XX:+DisableExplicitGC。这个参数的意思就是禁止显式执行GC,不允许你来通过代码触发GC System.gc()
OOM
Metaspace区域里就会发生OOM
-XX:MetaspaceSize=512m
cglib动态分配类容易发生
-XX:MaxMetaspaceSize=512m
虚拟机栈发生OOM
递归调用方法发容易发生
堆内存发生OOM
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/usr/local/app/oom
-Xms4096M -Xmx4096M -Xmn3072M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFaction=92 -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSParallelInitialMarkEnabled -XX:+CMSScavengeBeforeRemark -XX:+DisableExplicitGC -XX:+PrintGCDetails -Xloggc:gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/app/oom
MySQL
体系架构
Client Connectors层
处理客户端的连接请求
MySQL Server层
Connection Pool
负责处理和存储数据库与客户端创建的连接
Service & utilities
管理服务&工具集,包括备份恢复、安全管理、集群管理服务和工具
SQL interface
负责接收客户端发送的各种 SQL 语句
Parser解析器
对 SQL 语句进行语法解析生成解析树
Optimizer 查询优化器
查询优化器会根据解析树生成执行计划,并选择合适的索引,然后按照执行计划执行 SQL 语言并与各个存储引擎交互
Caches 缓存
包括各个存储引擎的缓存部分
InnoDB 存储的 Buffer Pool、MyISAM 存储引擎的 key buffer 等,Caches 中也会缓存一些权限,也包括一些 Session 级别的缓存
InnoDB 存储的 Buffer Pool、MyISAM 存储引擎的 key buffer 等,Caches 中也会缓存一些权限,也包括一些 Session 级别的缓存
存储引擎层
MyISAM
InnoDB
内存类型的 Memory
归档类型的 Archive
列式存储的 Infobright
是文件的物理存储层,包括二进制日志、数据文件、错误日志、慢查询日志、全日志、redo/undo 日志
存储引擎
支持事务的 InnoDB(5.6 版本以后默认)
MVCC、锁、锁算法和分类、事务、表空间和数据页、内存线程以及状态查询
Write Ahead Logging
先写日志后写磁盘,日志成功写入后事务就不会丢失,后续由 checkpoint 机制来保证磁盘物理文件与 Redo 日志达到一致性
利用 Redo 记录变更后的数据,即 Redo 记录事务数据变更后的值
利用 Undo 记录变更前的数据
SQL语句解析执行过程
事务与锁
ACID:原子性、一致性、隔离性和持久性
一致性:事务开始之前和事务结束之后,数据库的完整性限制未被破坏
原子性:事务的所有操作,要么全部完成,要么全部不完成,不会结束在某个中间环节
持久性:事务完成之后,事务所做的修改进行持久化保存,不会丢失
隔离性:当多个事务并发访问数据库中的同一数据时,所表现出来的相互关系
读未提交(RU,Read Uncommitted)
读提交(RC,Read Committed)
可重复读(RR,Repeatable Read)
可串行化
并发事务控制
锁分类
表级锁
行级锁
页级锁
InnoDB 中的锁
共享锁(S)
排他锁(X
意向共享锁(IS)
意向排他锁(IX)
自增锁(AUTO-INC Locks)
InnoDB 行锁
Record Lock 锁:单个行记录的锁(锁数据,不锁 Gap)
Gap Lock 锁:间隙锁,锁定一个范围,不包括记录本身(不锁数据,仅仅锁数据前面的Gap)
Next-key Lock 锁:同时锁住数据,并且锁住数据前面的 Gap
InnoDB 死锁
互斥条件
请求与保持条件
不剥夺条件
循环等待条件
数据库表设计
范式
第一范式
第二范式
第三范式
反范式设计
业务场景
相应时间
字段冗余
基本设置规则
必须指定默认存储引擎为 InnoDB,并且禁用 MyISAM 存储引擎
默认字符集 UTF8mb4
关闭区分大小写功能。设置 lower_case_tables_name=1
规范命名
索引设计
索引原理
Hash 索引
B+Tree 索引
聚簇索引
辅助索引
索引类型
哈希索引(Memory/InnoDB adaptive Hash index/NDB)
B+Tree 索引(MyISAM/InnoDB)
全文索引(MyISAM/InnoDB)
空间索引(MyISAM R-Tree)
分形树索引(TokuDB Fractal Tree Index)
提高查询性能
MySQL 查询优化器
SELECT 执行过程
ICP
ICP 是 Index Condition Pushdown 的简称,是 MySQL 使用索引从表中检索行数据的一种优化方式。目的是减少从基表中全记录读取操作的数量,从而降低 IO 操作
MRR
MRR 是 Multi-Range Read 的简称,是 MySQL 优化器将随机 IO转化为顺序 IO 以降低查询过程中 IO 开销的一种手段
BKA 和 BNL
BKA 是 Batched Key Access 的简称,是 MySQL 优化器提高表 join 性能的一种手段,它是一种算法。而 BNL 是 Block Nested Loop 的简称,它是默认的处理表 join 的方式和算法
MySQL 执行计划分析
查看 SQL 执行计划
explain SQL;
desc 表名;
show create table 表名
desc 表名;
show create table 表名
通过 Profile 定位 QUERY 代价消耗
set profiling=1;
执行 SQL;
show profiles; 获取 Query_ID。
show profile for query Query_ID; 查看详细的 profile 信息
执行 SQL;
show profiles; 获取 Query_ID。
show profile for query Query_ID; 查看详细的 profile 信息
通过 Optimizer Trace 表查看 SQL 执行计划树
set session optimizer_trace='enabled=on';
执行 SQL;
查询 information_schema.optimizer_trace 表,获取 SQL 查询计划树;
set session optimizer_trace=‘enabled=off';开启此项影响性能,记得用后关闭
执行 SQL;
查询 information_schema.optimizer_trace 表,获取 SQL 查询计划树;
set session optimizer_trace=‘enabled=off';开启此项影响性能,记得用后关闭
数据库服务器硬件优化
CPU
系统配置选择 Performance Per Watt Optimized(DAPC)
CPU优先选择高主频以提高运算能力;其次选择核数多,可以多线程并发处理和多实例部署
关闭 C1E(增强型空闲电源管理状态转换)和 C states,DB 服务器不需要节能和省电运行,默认是开启状态,DB 服务器建议关闭以提高 CPU 效率
数据库服务器选择高主频多核数 CPU 类型,同时开启最大性能和关闭 CPU CIE 和 C States。 高频加速 SQL 执行,多核解决并发
内存
优先选择大内存,同时开启最大性能并关闭NUMA
参数优化
Redo Log
innodb_flush_log_at_trx_commit
Replication
主库 Master 将数据库的变更操作记录在二进制日志 Binary Log 中
备库 Slave 读取主库上的日志并写入到本地中继日志 Relay Log 中
备库读取中继日志 Relay Log 中的 Event 事件在备库上进行重放 Replay
性能优化
连接池优化
架构优化
缓存
集群
主从复制
分库分表
优化器
SQL语句分析优化
慢查询日志
show processlist
show status
explain
type类型
system>const>eq_ref>ref>range>index>all
SQL与索引优化
存储引擎
Tomcat
容器云原生
Docker
简介
概念
使用 Google 公司推出的 Go 语言 进行开发实现,基于 Linux 内核的 cgroup,namespace,以及 OverlayFS 类的 Union FS 等技术,对进程进行封装隔离,属于 操作系统层面的虚拟化技术。由于隔离的进程独立于宿主和其它的隔离的进程,因此也称其为容器。最初实现是基于 LXC,从 0.7 版本以后开始去除 LXC,转而使用自行开发的 libcontainer,从 1.11 版本开始,则进一步演进为使用 runC 和 containerd。
docker架构
优点
更高效的利用系统资源
更快速的启动时间
一致的运行环境
持续交付和部署
更轻松的迁移
更轻松的维护和扩展
基础概念
镜像
Docker 镜像 是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像 不包含 任何动态数据,其内容在构建之后也不会被改变。
容器
容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等
仓库
一个 Docker Registry 中可以包含多个 仓库(Repository);每个仓库可以包含多个 标签(Tag);每个标签对应一个镜像
安装
windows Pro
Docker Desktop for Windows
CentOS
添加 Docker 安装源
sudo yum-config-manager \
--add-repo \
https://download.docker.com/linux/centos/docker-ce.repo
--add-repo \
https://download.docker.com/linux/centos/docker-ce.repo
直接安装最新版本的 Docker
sudo yum install docker-ce docker-ce-cli containerd.io
启动 Docker
sudo systemctl start docker
使用镜像
获取镜像
获取
$ docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签]
运行
docker run
列出镜像
列出镜像
docker image ls
镜像体积
docker system df
虚悬镜像(dangling image)
docker image ls -f dangling=true
docker image prune
过滤
docker image ls -f since=mongo:3.2
docker image ls -f label=com.example.version=0.1
以特定格式显示
docker image ls -q
docker image ls --format "{{.ID}}: {{.Repository}}"
删除本地镜像
docker image rm [选项] <镜像1> [<镜像2> ...]
可以用镜像的完整 ID,也称为 长 ID,来删除镜像,也可以用短ID
利用 commit 理解镜像构成
使用 Dockerfile 定制镜像
FROM 指定基础镜像
空白的镜像FROM scratch
RUN 执行命令
镜像构建上下文
直接用 Git repo 进行构建
docker build -t hello-world https://github.com/docker-library/hello-world.git#master:amd64/hello-world
用给定的 tar 压缩包构建
docker build http://server/context.tar.gz
从标准输入中读取 Dockerfile 进行构建
docker build - < Dockerfile
cat Dockerfile | docker build -
从标准输入中读取上下文压缩包进行构建
docker build - < context.tar.gz
Dockerfile 指令详解
COPY 复制文件
COPY [--chown=<user>:<group>] <源路径>... <目标路径>
COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]
ADD 更高级的复制文件
Docker 引擎会试图去下载这个链接的文件放到 <目标路径> 去。下载后的文件权限自动设置为 600
CMD 容器启动命令
CMD ["nginx", "-g", "daemon off;"]
CMD [ "sh", "-c", "echo $HOME" ]
ENTRYPOINT 入口点
<ENTRYPOINT> "<CMD>"
场景一:让镜像变成像命令一样使用
场景二:应用运行前的准备工作
ENV 设置环境变量
ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...
ARG 构建参数
ARG <参数名>[=<默认值>]
VOLUME 定义匿名卷
EXPOSE 暴露端口
WORKDIR 指定工作目录
WORKDIR <工作目录路径>
USER 指定当前用户
USER <用户名>[:<用户组>]
HEALTHCHECK 健康检查
HEALTHCHECK [选项] CMD <命令>
HEALTHCHECK NONE
ONBUILD 为他人作嫁衣裳
ONBUILD <其它指令>
在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行
LABEL 为镜像添加元数据
LABEL <key>=<value> <key>=<value> <key>=<value> ...
SHELL 指令
SHELL ["executable", "parameters"]
参考文档
https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
https://docs.docker.com/engine/reference/builder/
https://github.com/docker-library/docs
Dockerfile 多阶段构建
构建多种系统架构支持的 Docker 镜像
操作容器
启动
新建并启动
docker run ubuntu:18.04 /bin/echo 'Hello world'
启动已终止容器
docker container start 命令
守护态运行
Docker 在后台运行而不是直接把执行命令的结果输出在当前宿主机下。此时,可以通过添加 -d 参数来实现
终止
docker container stop
Docker 容器中指定的应用终结时,容器也自动终止
进入容器
attach 命令
docker attach
exec 命令
docker exec -it 8482d5ad7d61 /bin/bash 当 -i -t 参数一起使用时,则可以看到我们熟悉的 Linux 命令提示符。
导出和导入
docker export导出本地某个容器
docker import 从容器快照文件中再导入为镜像
删除
删除容器
docker container rm
清理所有处于终止状态的容器
docker container prune
K8s
Kubernetes
什么是Kubernetes
Kubernetes,从官方网站上可以看到,它是一个工业级的容器编排平台。Kubernetes 这个单词是希腊语,它的中文翻译是“舵手”或者“飞行员”。也叫做“k8s”,它是通过将8个字母“ubernete ”替换为“8”而导致的一个缩写。
我们之前其实介绍过一个概念叫做 container,container 这个英文单词也有另外的一个意思就是“集装箱”。Kubernetes 也就借着这个寓意,希望成为运送集装箱的一个轮船,来帮助我们管理这些集装箱,也就是管理这些容器。这个就是为什么会选用 Kubernetes 这个词来代表这个项目的原因。
我们之前其实介绍过一个概念叫做 container,container 这个英文单词也有另外的一个意思就是“集装箱”。Kubernetes 也就借着这个寓意,希望成为运送集装箱的一个轮船,来帮助我们管理这些集装箱,也就是管理这些容器。这个就是为什么会选用 Kubernetes 这个词来代表这个项目的原因。
更具体一点滴来说:Kubernetes是一个自动化的容器编排平台,它负责应用的部署、应用的弹性以及应用的管理,这些都是基于容器的。
Kubernetes有以下几个核心功能:
介绍
1. 服务的发现与负载均衡;
2. 容器的自动装箱,我们也会把它叫做scheduling,就是“调度”,把一个容器放到一个集群的某一个机器上,
Kubernetes会帮助我们去做存储的编排,让存储的声明周期与容器的生命周期能有一个连接;
Kubernetes会帮助我们去做存储的编排,让存储的声明周期与容器的生命周期能有一个连接;
3. Kubernetes会帮助我们去做自动化的容器的恢复。在一个集群中,经常会出现宿主机的问题或者说OS的问题,
导致容器本身的不可用,Kubernetes会自动地对这些不可用的容器进行恢复;
导致容器本身的不可用,Kubernetes会自动地对这些不可用的容器进行恢复;
4. Kubernetes会帮助我们去做应用的自动发布与应用的回滚,以及与应用相关的配置密文的管理;
5. 对于job类型任务,Kubernetes可以去做批量的执行;
6. 为了让这个集群、这个应用更富有弹性,Kubernetes也支持水平的伸缩。
1. 调度
Kubernetes可以把用户提交的容器放到Kubernetes管理的集群的某一台节点上去。
Kubernetes的调度器是执行这项能力的组件,它会观察正在被调度的这个容器的大小、规格。
Kubernetes的调度器是执行这项能力的组件,它会观察正在被调度的这个容器的大小、规格。
比如说它所需要的CPU以及它所需要的memory,然后在集群中找一台相对比较空闲的机器来进行一次placement,
也就是一次放置的操作。在下面图片中,它可能会把红颜色的这个容器放置到第二台空闲的机器上,来完成一次调度的工作。
也就是一次放置的操作。在下面图片中,它可能会把红颜色的这个容器放置到第二台空闲的机器上,来完成一次调度的工作。
2. 自动修复
Kubernetes有一个节点健康检查的功能,它会检测这个集群中所有的宿主机,当宿主机本身出现故障,
或者软件出现故障的时候,这个节点健康检查会自动对它进行发现。
或者软件出现故障的时候,这个节点健康检查会自动对它进行发现。
下面Kubernetes会把运行在这些失败节点上的容器进行自动迁移,迁移到一个正在健康运行的宿主机上,来完成集群容器的一个自动恢复。
迁移
3. 水平伸缩
Kubernetes有业务负载检查的能力,它会监测业务上所承担的负载,如果这个业务本身的CPU利用率过高,
或者响应时间过长,它可能对这个业务进行一次扩容。
或者响应时间过长,它可能对这个业务进行一次扩容。
比如说在下面这个图片中,黄颜色的过度忙碌,Kubernetes就可以把黄颜色负载从一份变为三份。接下来,
它就可以通过负载均衡把原来打到第一个黄颜色上的负载平均分到三个黄颜色的负载上去,以此来提高响应的时间
它就可以通过负载均衡把原来打到第一个黄颜色上的负载平均分到三个黄颜色的负载上去,以此来提高响应的时间
水平扩容
Kubernetes的架构
Kubernetes架构是一个比较典型的二层架构和server-client架构。
Master作为中央的管控节点,会去与Node进行一次连接。
Master作为中央的管控节点,会去与Node进行一次连接。
所有UI的、clients、这些user侧的组件,只会和Master进行连接,把希望的状态或者想执行的命令
下发给Master,Master会把这些命令或者状态下发给相应的节点,进行最终的执行。
下发给Master,Master会把这些命令或者状态下发给相应的节点,进行最终的执行。
Kubernetes的Master包含四个主要的组件:
API Server、Controller、Scheduler以及etcd。如右图所示:
API Server、Controller、Scheduler以及etcd。如右图所示:
API Server:顾名思义是用来处理 API 操作的,Kubernetes中所有的组件都会和 API Server 进行连接,
组件与组件之间一般不进行独立的连接,都依赖于API Server 进行消息的传送;
组件与组件之间一般不进行独立的连接,都依赖于API Server 进行消息的传送;
Controller:是控制器,它用来完成对集群状态的一些管理。比如刚刚我们提到的两个例子之中,
第一个自动对容器进行修复、第二个自动进行水平扩张,都是由Kubernetes中的Controller来进行完成的;
第一个自动对容器进行修复、第二个自动进行水平扩张,都是由Kubernetes中的Controller来进行完成的;
Scheduler:是调度器,“调度器”顾名思义就是完成调度的操作,就是我们刚才介绍的第一个例子中,
把一个用户提交的Container,依据它对CPU、对memory请求大小,找一台合适的节点,进行放置;
把一个用户提交的Container,依据它对CPU、对memory请求大小,找一台合适的节点,进行放置;
etcd:是一个分布式的一个存储系统,API Server 中所需要的这些原信息都被放置在etcd中,
etcd本身是一个高可用系统,通过etcd保证整个Kubernetes的Master组件的高可用性。
etcd本身是一个高可用系统,通过etcd保证整个Kubernetes的Master组件的高可用性。
刚刚提到的API Server,它本身在部署结构上是一个可以水平扩展的一个部署组件;Controller是一个可以进行热备的一个部署组件,
它只有一个active,它的调度器也是相应的,虽然只有一个active,但是可以进行热备。
它只有一个active,它的调度器也是相应的,虽然只有一个active,但是可以进行热备。
Node
Kubernetes的Node是真正运行业务负载的,每个业务负载会以Pod的形式运行。下面会介绍Pod的。
一个Pod中运行的一个或者多个容器,真正去运行这些Pod的组件的是叫做Kubelet,也就是Node上最为关键的组件,
它通过API Server接收到所需要Pod运行的状态,然后提交到我们下面画的这个Controller Runtime组件中。
一个Pod中运行的一个或者多个容器,真正去运行这些Pod的组件的是叫做Kubelet,也就是Node上最为关键的组件,
它通过API Server接收到所需要Pod运行的状态,然后提交到我们下面画的这个Controller Runtime组件中。
在OS上去创建容器所需要运行的环境,最终把容器或者Pod运行起来,也需要对存储跟网络进行管理。Kubernetes并不会去直接进行网络存储的操作,它们会靠Storage Plugin或者是网络的Pligin来进行操作。用户自己或者云厂商都会去写相应的Storage Plugin或者Network Plugin,去完成存储操作与网络操作。
在Kubernetes自己的环境中,也会有Kubernetes的Network,它是为了提供Service network来进行搭网组网的。(等一下会介绍“service”这个概念。)真正完成service组网的组件就是Kube-proxy,它是利用了iptable的能力来进行组建Kubernetes的Network,就是cluster network,以上就是Node的四个组件。
Kubernetes的Node并不会直接和user进行interaction,它的interaction只会通过Master。
而user是通过Master向节点下发这些信息的。Kubernetes每个Node上,都会运行上面的几个组件。
而user是通过Master向节点下发这些信息的。Kubernetes每个Node上,都会运行上面的几个组件。
下面我们以一个例子再去看一下Kubernetes架构中的这些组件,
是如何互相进行interaction的。
是如何互相进行interaction的。
用户可以通过UI或者CLI提交一个Pod给Kubernetes进行部署,这个Pod请求会首先通过CLI或者UI提交给Kubernetes API Server,下一步API Server会把这个信息写入到它的存储系统etcd,之后Scheduler会通过API Server的watch或者叫做notification机制得到这个信息:有一个Pod需要被调度。
这个时候Scheduler会根据它的内存状态进行一次调度决策,在完成这次调度之后,它会向API Server report说“OK!这个Pod需要被调度到某一个节点上。”
这个时候API Server接收到这次操作之后,会把这次的结果再次写到etcd中,然后API Server会通知相应的节点进行这次Pod真正的执行启动。相应节点的Kubelet会得到这个通知,Kubelet就会去调Container runtime来真正去启动配置这个容器和这个容器的运行环境,去调度Storage Plugin去配置存储,network Plugin去配置网络。
这个例子我们可以看到:这些组件之间是如何相互沟通相互通信,协调来完成一次Pod的调度执行操作的。
Kubernetes的核心概念与它的API
核心概念
Pod
Pod是Kubernetes的一个最小调度以及资源单元。用户可以通过Kubernetes的Pod API 生产一个Pod,让Kubernetes对这个Pod进行调度,也就是把它放在某一个Kubernetes管理的节点上运行起来。一个Pod简单来说是对一组容器的抽象,它里面会包含一个或者多个容器。
比如像右边这副图里面,它包含了两个容器,每个容器可以指定它所需要的资源大小。
比如说,一核一G,或者0.5核0.5G。
比如说,一核一G,或者0.5核0.5G。
当然在这个Pod中也可以包含一些其它所需要的资源:比如说我们所看到的Volume卷这个存储资源;
比如说我们需要100GB的存储或者20GB的另外一个存储。
比如说我们需要100GB的存储或者20GB的另外一个存储。
在Pod里面,我们也可以去定义容器所需要运行的方式。比如说运行容器的Command,以及运行容器的环境变量等等。Pod这个抽象也给这些容器提供了一个共享的运行环境,它们会共享同一个网络环境,这些容器可以用localhost来进行直接的连接。而Pod与Pod之间,是互相有isolation隔离的。
Volume
Volume就是卷的概念,它是用来管理Kubernetes存储的,是用来声明在Pod中容器可以访问文件目录的,
一个卷可以被挂载在Pod中一个或者多个容器的指定路径下面。
一个卷可以被挂载在Pod中一个或者多个容器的指定路径下面。
而Volume本身是一个抽象的概念,一个Volume可以去支持多种的后端的存储。比如说Kubernetes的Volume就支出了很多存储插件,它可以支持本地的存储,可以支持分布式的存储,比如说像ceph,GlusterFS;它也可以支出云存储,比如说阿里云上的云盘、AWS上的云盘、Google上的云盘等等。
Deployment
Deployment是在Pod这个抽象上更为上层的一个抽象,它可以定义一组Pod的副本数目、以及这个Pod的版本。
一般大家用Deployment这个抽象来做应用的真正管理,而Pod是组成Deployment的最小的单元。
一般大家用Deployment这个抽象来做应用的真正管理,而Pod是组成Deployment的最小的单元。
Kubernetes是通过Controller,也就是我们刚才提到的控制器去维护Deployment中Pod的数目,它也会去帮助Deployment自动恢复失败的Pod。
比如说我可以定义一个Deployment,这个Deployment里面需要两个Pod,当一个Pod失败的时候,控制器就会检测到,它重新把Deployment中的Pod数目从一个恢复到两个,通过再去新生成一个Pod。通过控制器,我们也会帮助完成发布的策略。比如说进行滚动升级,进行重新生成的升级,或者进行版本的回滚。
Service
Service提供了一个或者多个Pod实例的稳定访问地址。
比如在上面的例子中,我们看到:一个Deployment可能有两个甚至更多个完全相同的Pod。对于一个外部的用户来讲,访问哪个Pod其实都是一样的,所以它希望做一次负载均衡,在做负载均衡的同时,我只想访问一个固定的VIP,也就是Virtual IP地址,而不希望得知每一个具体的Pod的IP地址。
刚才提到,这个Pod本身可能terminal go(终止),如果一个Pod失败了,可能会换成另外一个新的。
对一个外部用户来讲,提供了多个具体的Pod地址,这个用户要不停地去更新Pod地址,当这个Pod再失败重启之后,我们希望有一个抽象,把所有Pod的访问能力抽象成一个第三方的一个IP地址,实现这个的Kubernetes的抽象就叫做Service。
实现Service的方式有多种,Kubernetes支出Cluster IP,上面我们讲过的Kuber-proxy的组网,
它也支持nodePort、LoadBalancer等其它的一些访问的能力。
它也支持nodePort、LoadBalancer等其它的一些访问的能力。
NamesPace
Namespace是用来做一个集群内部的逻辑隔离的,它包括鉴权、资源管理等。Kubernetes的每个资源,比如刚才讲的Pod、Depolyment、Service都属于一个Namespace,同一个Namespace中的资源需要命名的唯一性,不同的Namespace中的资源可以重名。
Namespace一个用例,比如像在阿里巴巴,我们内部会有很多个business units,在每一个business units之间,希望有一个视图上的隔离,并且在鉴权上也一样,在cuda上面也不一样,我们就会用Namespace来去给每一个BU提供一个他所看到的这么一个看到的隔离的机制。
API
从high-level上看,Kubernetes API是由HTTP+JSON组成的:用户访问的方式是HTTP,访问的API中content的内容是JSON格式的。
Kubernetes的kubectl,也就是command tool,Kubernetes UI,或者有时候用curl,
直接与Kubernetes进行沟通,都是使用HTTP+JSON这种格式。
直接与Kubernetes进行沟通,都是使用HTTP+JSON这种格式。
下面这个例子:比如说,对于这个Pod资源,它的HTTP访问的路径,就是API,然后是apiversion: V1,
之后是相应的Namespace,以及Pods资源,最终是Podname,也就是Pod的名字。
之后是相应的Namespace,以及Pods资源,最终是Podname,也就是Pod的名字。
如果我们去提交一个Pod,或者get一个Pod的时候,它的content内容都是用JSON或者是YAML表达的。上图中有个yaml的例子,在这个yaml file中,对Pod资源的描述也分为几个部分。
第一个部分,一般来讲会是 API 的 version。比如在这个例子中是V1,它也会描述我在操作哪个资源;比如说我们的kind如果是Pod,在Metadata中,就写上这个Pod的名字;比如说nginx,我们也会给它打一些label,我们等下会讲label的概念。在Metadata中,有时候也会去写annotation,也就是对资源的额外的一些用户层次的描述。
比较重要的一个部分叫做Spec,Spec也就是我们希望Pod达到一个预期的状态。比如说它内部需要有哪些container被运行;比如说这里面有一个nginx的container,它的image是什么?它暴露的port是什么?
当我们从Kubernetes API中去获取这个资源的时候,一般来讲在Spec下面会有一个项目叫status,它表达了这个资源当前的状态;比如说一个Pod的状态可能是正在被调度、或者是已经running、或者是已经被terminates,就是被执行完毕了。
在刚刚API之中,我们讲了一个比较有意思的metadata叫做“label”,这个label可以是一组KeyValuePair。
比如下图的第一个Pod中,label就可能是一个color等于red,即它的颜色是红颜色。当然你也可以加其它label,比如说size: big就是大小,定义为大的,它可能是一组label。
这些label是可以被selector,也就是选择器所查询的。这个能力实际上跟我们的sql类型的select语句是非常相似的,比如下图中的三个Pod资源中,我们就可以进行select。name color等于red,就是它的颜色是红色的,我们也可以看到,只有两个被选中了,因为只有他们的label是红色的,另外一个label中写的color等于yellow,也就是它的颜色是黄色,是不会被选中的。
通过label ,kubernetes的API层就可以对这些资源进行一个统一的筛选,那这些筛选也是kubernetes对资源的集合所表达默认的一种方式。
例如说,我们刚刚介绍的Deployment,它可能是代表一组Pod,它是一组Pod的抽象,一组Pod就是通过label selector来表达的。当然我们刚才讲到说service对应的一组Pod,就是一个service要对应一个或者多个的Pod,来对它们进行统一的访问,这个描述也是通过label selector来进行select选取一组Pod。
所以可以看到label是一个非常核心的kubernetes API的概念。
以一个demo结尾
安装一个 Kubernetes 沙箱环境
首先需要安装一个虚拟机,来在虚拟机中启动 Kubernetes。
我们会推荐大家利用 virtualbox 来作为虚拟机的运行环境;
我们会推荐大家利用 virtualbox 来作为虚拟机的运行环境;
安装 VirtualBox: https://www.virtualbox.org/wiki/Downloads
其次我们需要在虚拟机中启动 Kubernetes,Kubernetes 有一个非常有意思的项目,叫 minikube,
也就是启动一个最小的 local 的 Kubernetes 的一个环境。
也就是启动一个最小的 local 的 Kubernetes 的一个环境。
minikube 我们推荐使用下面写到的阿里云的版本,它和官方 minikube 的主要区别就是把 minikube 中
所需要的 Google 上的依赖换成国内访问比较快的一些镜像,这样就方便了大家的安装工作;
所需要的 Google 上的依赖换成国内访问比较快的一些镜像,这样就方便了大家的安装工作;
安装 MiniKube(中国版): https://yq.aliyun.com/articles/221687
最后在安装完 virtualbox 和 minikube 之后,大家可以对 minikube 进行启动,也就是下面这个命令。
启动命令:minikube start —vm-driver virtualbox
如果大家不是 Mac 系统,其他操作系统请访问下面这个链接,查看其它操作系统如何安装 minikube 沙箱环境。
https://kubernetes.io/docs/tasks/tools/install-minikube/
https://kubernetes.io/docs/tasks/tools/install-minikube/
做一个例子,来做三件事情:
1. 提交一个 nginx deployment;
kubectl apply -f https://k8s.io/examples/application/deployment.yaml
2. 升级 nginx deployment;
kubectl apply -f https://k8s.io/examples/application/deployment-update.yaml
3. 扩容 nginx deployment。
kubectl apply -f https://k8s.io/examples/application/deployment-scale.yaml
Service Mesh
云原生
简介
云原生的定义
实际上,云原生是一条最佳路径或者最佳实践。更详细的说,云原生为用户指定了一条低心智负担的、敏捷的、
能够以可扩展、可复制的方式最大化地利用云的能力、发挥云的价值的最佳路径
能够以可扩展、可复制的方式最大化地利用云的能力、发挥云的价值的最佳路径
云原生的核心底盘
容器技术
容器技术使得应用具有了一种“自包含”的定义方式。所以,这样的应用才能以敏捷的、以可扩展可复制的方式发布在云上,
发挥出云的能力。这也就是容器技术对云发挥出的革命性影响所在。所以说,容器技术是云原生技术的核心地盘。
发挥出云的能力。这也就是容器技术对云发挥出的革命性影响所在。所以说,容器技术是云原生技术的核心地盘。
云原生的技术范畴
第一部分:是云应用定义与开发流程。这包括应用定义与镜像制作、配置CI/CD、消息和Streaming以及数据库等。
第二部分:是云应用的编排与管理流程。这也是Kubernetes比较关注的一部分,
包括了应用编排与调度、服务发现治理、远程调用、API网关以及Service Mesh。
包括了应用编排与调度、服务发现治理、远程调用、API网关以及Service Mesh。
第三部分:是监控与可观测性。这部分所强调的是云上应用如何进行监控、日志收集、Tracing以及在云上如何实现破坏性测试,也就是混沌工程的概念。
第四部分:就是云原生的底层技术,比如容器运行时、云原生存储技术、云原生网络技术等。
第五部分:是云原生工具集,在前面的这些核心技术点之上,还有很多配套的生态或者周边的工具需要使用,
比如流程自动化与配置管理、容器镜像仓库、云原生安全技术以及云端密码管理等。
比如流程自动化与配置管理、容器镜像仓库、云原生安全技术以及云端密码管理等。
最后则是Serverless。Serverless是一种PaaS的特殊形态,它定义了一种更为“极端抽象”的应用编写方式,包含了FaaS和BaaS这样的概念。
而无论是FaaS还是BaaS,其最为典型的特点就是按实际使用计费(Pay as you go),因此Serverless计费也是重要的知识和概念。
而无论是FaaS还是BaaS,其最为典型的特点就是按实际使用计费(Pay as you go),因此Serverless计费也是重要的知识和概念。
云原生思想的两个理论
不可变基础设施
这一点目前通过容器镜像来实现的,其含义就是应用的基础设施应该是不可变的,是一个自包含、自描述可以完全在不同环境中迁移的东西;
云应用编排理论
当前的实现方式就是Google所提出来的“容器设计模式”
容器与镜像
什么是容器?
回顾一下操作系统是如何管理进程的
首先,当我们登陆到操作系统之后,可以通过ps等操作看到各式各样的的进程,
这些进程包括系统自带的服务和用户的应用进程。那么,这些进程都有什么样的特点?
这些进程包括系统自带的服务和用户的应用进程。那么,这些进程都有什么样的特点?
第一、这些进程可以相互看到、相互通信;
第二、它们使用的是同一个文件系统,可以对同一个文件进行读写操作;
第三、这些进程会使用相同的系统资源。
这三个特点会带来什么问题呢?
因为这些进程能够好像看到并且进行通信,高级权限的进程可以攻击其它进程;
因为它们使用的是同一个文件系统,因此会带来两个问题:这些进程可以对于已有数据进行增删改查,具有高级权限的进程可能会将其它进程的数据删除掉,破坏其它进程的正常运行;此外,进程与进程之间的依赖可能会存在冲突,如此一来就会给运维带来很大压力;
因为这些进程使用的是同一个宿主机的资源,应用之间会存在资源抢占的问题,当一个应用需要消耗大量的CPU和内存资源的时候,
就可能会破坏其它应用的运行,导致其它应用无法正常地提供服务。
就可能会破坏其它应用的运行,导致其它应用无法正常地提供服务。
针对上述三个问题,如何为进程提供一个独立的运行环境呢?
针对不同进程使用同一个文件系统所造成的问题而言,Linux和Unix操作系统可以通过chroot系统调用将子目录变成根目录,达到视图级别的隔离;
进程在chroot的帮助下可以具有独立的文件系统,对于这样的文件系统进行增删改查不会影响到其它进程;
进程在chroot的帮助下可以具有独立的文件系统,对于这样的文件系统进行增删改查不会影响到其它进程;
因为进程之间相互可见并且可以相互通信,使用Namespace技术来实现进程在资源的视图上进行隔离。
在chroot和Namespace的帮助下,进程就能够运行在一个独立的环境下了。
在chroot和Namespace的帮助下,进程就能够运行在一个独立的环境下了。
但在独立的环境下,进程所使用的还是同一个操作系统的资源,一些进程可能会侵蚀掉整个操作系统的资源。
为了减少进程彼此之间的影响,可以通过Cgroup来限制其资源的使用率,设置其能够使用的CPU以及内存量。
为了减少进程彼此之间的影响,可以通过Cgroup来限制其资源的使用率,设置其能够使用的CPU以及内存量。
那么,应该如何定义这样的进程集合呢?
其实,容器就是一个试图隔离、资源可限制、独立文件系统的进程集合。
所谓“视图隔离”就是能够看到部分进程以及具有独立的主机名等;控制资源使用率则是可以对于内存大小以及CPU使用个数等进行限制。
容器就是一个进程集合,它将系统的其它资源隔离开来,具有自己独立的资源视图。
容器就是一个进程集合,它将系统的其它资源隔离开来,具有自己独立的资源视图。
容器具有一个独立的文件系统,因为是使用的是系统资源,所以在独立的文件系统内不需要具备内核相关代码或者工具,
我们只需要提供容器所需的二进制文件、配置文件以及依赖即可。只要容器运行时所需的文件集合都能够具备,那么这个容器就能够运行起来。
我们只需要提供容器所需的二进制文件、配置文件以及依赖即可。只要容器运行时所需的文件集合都能够具备,那么这个容器就能够运行起来。
什么是镜像?
我们将这些容器运行时所需要的所有文件集合称之为镜像
采用什么样的方式来构建镜像呢?
通常情况下,我们采用Dockerfile来构建镜像,这是因为Dockerfile提供了非常便利的语法糖,能够帮助我们很好的描述构建的每个步骤。
当然,每个构建步骤都会对已有的文件系统进行操作,这样就会带来文件系统内容的变化,我们将这些变化称之为changeset。
当我们把构建步骤所产生的变化依次作用到一个空文件夹上,就能够得到一个完整的镜像。
当然,每个构建步骤都会对已有的文件系统进行操作,这样就会带来文件系统内容的变化,我们将这些变化称之为changeset。
当我们把构建步骤所产生的变化依次作用到一个空文件夹上,就能够得到一个完整的镜像。
changeset的分层以及复用特点能够带来几点优势:
第一、能够提高分发效率,简单试想一下,对于大的镜像而言,如果将其拆分成各个小块就能够提高镜像的分发效率,
这是因为镜像拆分之后就可以并行下载这些数据;
这是因为镜像拆分之后就可以并行下载这些数据;
第二、因为这些数据是相互共享的,也就意味着当本地存储上包含了一些数据的时候,只需要下载本地没有的数据即可,举个简单点例子就是golang镜像是基于alpine镜像进行构建的,当本地已经具有alpine镜像之后,再下载golang镜像的时候只需要下载本地alpine镜像中没有的部分即可;
第三、因为镜像数据是共享的,因此可以节约大量的磁盘空间,简单设想一下,当本地存储具有了alpine镜像和golang镜像,在没有复用的能力之前,alpine镜像具有5M大小,golang镜像具有300M大小,因此就会占用305M空间;而当具有了复用能力之后,只需要300M空间即可。
那如何构建镜像呢?
编写Dockerfile文件
FROM golang:1.12-alpine
WORKDIR /go/src/app
COPY . .
RUN go get -d -v ./..
RUN go install -v ./..
CMD ["app"]
WORKDIR /go/src/app
COPY . .
RUN go get -d -v ./..
RUN go install -v ./..
CMD ["app"]
FROM行表示一下的关键步骤基于什么镜像进行的构建,正如前面所说的,镜像是可以复用的;
WORKDIR行表示会把接下来的关键步骤都在哪一个相应的具体目录下进行,相当于cd到哪个文件夹一样;
COPY行表示的是可以将宿主机上的文件拷贝到容器镜像内,还有一个ADD也可以,有一点区别的哦;
RUN行表示在具体的文件系统内执行相应的动作。当我们运行完毕之后就可以得到一个应用了;
CMD行表示使用镜像时的默认程序名字。
当有了Dockerfile之后,就可以提供docker build -t .命令构建所需要的应用。
构建的结果存储在本地,一般情况下,镜像构建会在打包机或者其它隔离环境下完成。
构建的结果存储在本地,一般情况下,镜像构建会在打包机或者其它隔离环境下完成。
那么,这些镜像如何运行在生产环境或者测试环境上呢?这时候就需要一个中转站或者中心存储,我们称之为docker registry,也就是镜像仓库,其负责存储所有产生的镜像数据。我们只需要通过docker push就能够将本地的镜像推到镜像仓库中,这样一来,就能够在生产环境或者测试环境上将相应的数据下载下来并运行了。
如何运行容器?
第一步、从镜像仓库中将相应的镜像下载下来;
第二步、当镜像下载完成之后就可以通过docker images查看本地镜像;
第三步、当选中镜像之后,就可以通过docker run来运行这个镜像得到想要的容器,当然可以通过多次运行得到多个容器。一个镜像就相当于是一个模板,一个容器就像是一个具体的运行实例,因此镜像就具有了一次构建、到处运行的特点。
容器的生命周期
容器是一组具有隔离特性的进程集合,在使用docker run的时候会选择一个镜像来提供独立的文件系统并指定相应的运行程序。这里指定的运行程序称之为initial进程,这个initial进程启动的时候,容器也会随之启动,当initial进程退出的时候,容器也会随之退出。
因此,可以认为容器的生命周期和initial进程的生命周期是一致的。当然,因为容器内不只有这样一个这样的一个initial进程,initial进程本身也可以产生其它的子进程或者通过docker exec产生出来的运维操作,也属于initial进程管理的范围内。当initial进程退出的时候,所有的子进程也会随之退出,这样也是为了防止资源的泄漏。
但是这样的做法也会存在一些问题,首先应用里面的程序往往是有状态的,其可能会产生一些重要数据,当一个容器退出被删除之后,数据也就会丢失了,这对于应用方而言是不能接受的,所以需要将容器所产生出来的重要数据持久化下来。容器能够直接将数据持久化到指定的目录上,这个目录就称之为数据卷。
数据卷有一些特点,其中非常明显的就是数据卷的生命周期是独立于容器的生命周期,也就是说容器的创建、运行、停止、删除等操作都和数据卷没有任何关系,因为它是一个特殊的目录,是用于帮助容器进行持久化的,简单而言,我们会将数据卷挂载到容器内,这样一来容器就能够将数据写入到相应的目录里面了,而容器的退出并不会导致数据的丢失。
通常情况下,数据卷管理主要有两种方式:
第一种是通过binf的方式,直接将宿主机的目录直接挂载到容器内;这种方式比较简单,
但是会带来运维成本,因为其依赖宿主机的目录,需要对于所有的宿主机进行统一的管理。
但是会带来运维成本,因为其依赖宿主机的目录,需要对于所有的宿主机进行统一的管理。
第二种是将目录管理交给运行引擎。
容器项目架构
moby容器引擎架构
moby daemon会对上提供有关于容器、镜像、网络以及Volume的管理。
moby daemon所依赖的最重要的组件就是containerd,containerd是一个容器运行时管理引擎,其独立于moby daemon,可以对上提供容器、镜像的相关管理。
containerd底层有containerd shim模块,其类似于一个守护进程,这样设计的原因有几点:
首先,containerd需要管理容器的生命周期,而容器可能是由不同的容器运行时所创建出来的,因此需要提供一个灵活的插件化管理。
而shim就是针对于不同的容器运行时所开发的,这样就能够从containerd中脱离出来,通过插件的形式进行管理。
而shim就是针对于不同的容器运行时所开发的,这样就能够从containerd中脱离出来,通过插件的形式进行管理。
其次,因为shim插件化的实现,使其能够被containerd动态接管。如果不具备这样的能力,当moby deamon或者
containerd deamon意外退出的时候,容器就没人管理了,那么它它也会随之消失、退出,这样就会影响到应用的运行。
containerd deamon意外退出的时候,容器就没人管理了,那么它它也会随之消失、退出,这样就会影响到应用的运行。
最后,因为随时可能会对moby或者containerd进行升级,如果不提供shim机制,那么就无法做到原地升级,
也无法做到不影响业务的升级,因此containerd shim非常重要,它实现了动态接管的能力。
也无法做到不影响业务的升级,因此containerd shim非常重要,它实现了动态接管的能力。
容器 vs VM
VM利用Hypervisor虚拟化技术来模拟CPU、内存等硬件资源,这样就可以在宿主机上建立一个Guest OS,这是常说的安装一个虚拟机。
每一个Guest OS都有一个独立的内核,比如Ubuntu、CentOS甚至是Windows等,在这样的Guest OS之下,每一个应用都是相互独立的,VM可以提供一个更好的隔离效果。但这样的隔离效果需要付出一定的代价,因为需要把一部分的计算资源交给虚拟化,这样就很难充分的利用现有的计算资源,并且每个Guest OS都需要占用大量的磁盘空间,比如Windows操作系统的安装需要占用10-30G的磁盘空间,Ubuntu也需要5-6G,同时这样的方式启动很慢。正是因为虚拟机技术的缺点,催生出了容器技术。
容器是针对与进程而言的,因此无需Guest OS,只需要一个独立的文件系统提供所需要文件集合即可。所有的文件隔离都是进程级别的,因此启动时间快于VM,并且所需的磁盘空间也小于VM。当然了,进程级别的隔离并没有想象中的那么好,隔离效果比VM要差很多。
总体而言,容器和VM相比,各有优劣,因此容器技术也在向着强隔离方向发展。
Pod和容器设计模式
为什么需要Pod?
容器的基本概念
我们知道Pod是Kubernetes项目里面非常重要的概念,也是非常重要的一个原子调度单位,但是为什么我们会需要这样一个概念呢?
我们在使用容器Docker的时候,也没有这个说法。其实要理解Pod,我们首先要理解容器。
我们在使用容器Docker的时候,也没有这个说法。其实要理解Pod,我们首先要理解容器。
容器的本质实际上是一个进程,是一个视图被隔离,资源受限的进程。
容器里面 PID=1 的进程就是应用本身,这意味着管理虚拟机等于管理基础设施,因为我们是在管理机器,但管理容器却等于直接管理应用本身。这也是之前说过的不可变基础设施的一个最佳体现,这个时候,你的应用就等于你的基础设施,它一定是不可变的。
那Kubernetes又是什么呢?
Kubernetes就是云时代的操作系统!
以此类推,容器镜像其实就是:这个操作系统的软件安装包
真实操作系统里的例子
如果说Kubernetes就是操作系统的话,我们看一下真实操作系统的例子
例子里面有一个程序叫做Helloword,这个Helloword程序实际上是由一组进程组成的,
需要注意一下,这里说的进程实际上等同于Linux中的线程。
需要注意一下,这里说的进程实际上等同于Linux中的线程。
因为Linux中的线程是轻量级进程,所有如果从Linux系统中去查看helloword中的pstree,将会看到这个Helloword实际上是由四个线程组成的;分别是{api、main、log、compute}。也就是说,四个这样的线程共同协作,共享Helloword程序的资源,组成了Helloword程序的真实工作情况。
思考一下,在真实的操作系统里面,一个程序往往是根据进程组来进行管理的。Kubernetes把它类比为一个操作系统,比如说Linux。针对于容器我们前面提到可以类比为进程,就是前面的Linux线程。那么Pod又是什么呢?实际上Pod就是我们刚刚提到的进程组,也就是Linux里的线程组。
进程组概念
还是前面那个例子:Helloword程序由四个进程组成,这些进程之间会共享一些资源和文件。
那么现在有一个问题:假如说现在把Halloword程序用容器跑起来,你会怎么去做?
那么现在有一个问题:假如说现在把Halloword程序用容器跑起来,你会怎么去做?
当然,最自然的一个解法就是,我现在就启动一个Docker容器,里面运行四个进程。可是这样会有一个问题,这种情况下容器里面PID=1的进程该是谁?比如说,它应该是我的main进程,那么问题来了,“谁”又负责去管理剩余的三个进程呢?
这个核心问题在于,容器的设计本身是一种“单进程”模型,不是说容器里只能起一个进程,由于容器的应用等于进程,所以只能去管理PID=1的这个进程,其它再起来的进程其实是一个托管状态。所以说服务应用进程本身就具有“进程管理”的能力。
比如说Helloword的程序有system的能力,或者直接把容器里PID=1的进程直接改为systemd,否则这个应用,或者是容器是没有办法去管理很多个进程的。因为PID=1进程是应用本身,如果现在把这个PID=1的进程kill了,或者它自己运行过程中死掉了,那么剩下的三个进程的资源就没有人回收了,这个是非常严重的一个问题。
而反过来真的把应用本身改成了systemd,或者在容器里面运行了一个systemd,将会导致另外一个问题:使得管理容器,不再是管理应用本身了,而等于管理systemd,这里的问题就非常明显了。比如说我这个容器里面run的程序或者进程是systemd,那么接下来,这个应用是不是退出了?是不是fail了?是不是出现异常失败了?实际上是没办法直接知道的,因为容器管理的是systemd。这就是为什么在容器里面运行复杂程序往往比较困难的一个原因。
这里再梳理一下:由于容器实际上是一个“单进程”模型,所以如果你在容器里启动多个进程,只有一个可以作为PID=1的进程,而这时候,如果PID=1的进程挂了,或者说失败退出了,那么其它三个进程就会自然而然的成为孤儿,没有人能够管理它们,没有人能够回收它们的资源,这是一个非常不好的情况。
注意:Linux容器的“单进程”模型,指的是容器的生命周期等同于PID=1的进程(容器应用进程)的生命周期,而不是说容器里不能创建多进程。当然,一般情况下,容器应用进程并不具备进程管理能力,所以你通过exec或者ssh在容器里创建的其他进程,一旦异常退出(比如ssh终止)是很容易变成孤儿进程的。
反过来,其实可以在容器里面run 一个systemd,用它来管理其他所有的进程。这样会产生第二个问题:实际上没有办法直接管理我的应用了,因为我的应用被system给接管了,那么这个时候应用状态的生命周期就不等于容器的生命周期。这个管理模型实际上是非常非常复杂的。
Pod = "进程组"
在Kubernetes里面,Pod实际上正是Kubernetes项目为你抽象出来的一个可以类比为进程组的概念。
前面提到的,由四个进程共同组成的一个应用Helloword,在Kubernetes里面,实际上会被定义为一个拥有四个容器的Pod,这个一定要非常仔细的理解。
就是说现在有四个职责不同、相互协作的进程,需要放在容器里去运行,在Kubernetes里面并不会把它们放到一个容器里面,因为这里会遇到两个问题。那么在Kubernetes里会怎么去做呢?它会把四个独立的进程分别用四个独立的容器启动起来,然后把它们定义在一个Pod里面。
所以说当Kubernetes把Helloword给拉起来的时候,你实际上会看到四个容器,它们共享了某些资源,这些资源都属于Pod,所以说Pod在Kubernetes里面只有一个逻辑单位,没有一个真实的东西对应说这个就是Pod,不会有的。真正起来在物理上存在的东西,就是四个容器。这四个容器,或者说是多个容器的组合就叫做Pod。并且还有一个概念一定要非常明确,Pod是Kubernetes分配资源的一个单位,因为里面的容器要共享某些资源,所以Pod也是Kubernetes的原子调度单位。
这些应用之前往往有着密切的协作关系,使得它们必须部署在同一台机器上并且共享某些信息。
为什么Pod必须是原子调度单位?
虽然了解了这个东西是一个进程组,但是为什么要把Pod本身作为一个概念抽象出来呢?或者说能不能通过调度把Pod这个事情给解决掉呢?为什么Pod必须是Kubernetes里面的原子调度单位?
我们通过一个例子来解释:
假如现在有两个容器,它们是紧密协作的,所以它们应该被部署在一个Pod里面。具体来说,第一个容器叫做APP,就是业务容器,
它会写日志文件;第二个容器叫做LogCollector;它会把刚刚App容器写的日志文件转发到后端的ElasticSearch中。
它会写日志文件;第二个容器叫做LogCollector;它会把刚刚App容器写的日志文件转发到后端的ElasticSearch中。
两个容器的资源需求是这样的:App容器需要1G内存,LogCollector需要0.5G内存,而当前集群环境的可用内存是这样一个情况:Node_A:1.25G内存,Node_B:2G内存。
假如说现在没有Pod概念,就只有两个容器,这两个容器要紧密协作、运行在一台机器上。可是,如果调度器先把App调度到了Node_A上面,接下来会怎么样呢?这时你会发现:LogCollector实际上是没办法调度到Node_A上的,因为资源不够。其实此时整个应用本身就已经出问题了,调度已经失败了,必须去重新调度。
这是一个非常典型的成组调度失败的例子。英文名字叫做:Task co-scheduling问题,这个问题不是不能解,在很多项目里面,这样的问题都有解法。
比如说在Mesos里面,它会这一件事情,叫做资源囤积(resource hoarding):即当所以设置了Affinity约束的任务都达到时,才开始统一调度,这是一个典型的成组调度的解法。
所以上面提到的“App”和“LogCollector”这两个容器,在Mesos里面,它们不会说立即调度,而是等两个容器都提交完成,才开始统一调度。这样也会带来新的问题,首先调度效率会损失,因为需要等待。由于需要等还会有另外一个情况出现,就是产生死锁,就是互相等待的一个情况。这些机制在Mesos里都是需要解决的,也带来了额外的复杂度。
另外一种解法是Google的解法。它在Omega系统(就是Borg下一代)里面,做了一个非常复杂且非常厉害的解法,叫做乐观调度。比如说:不管这些冲突的异常情况,先调度,同时设置一个非常精妙的回滚机制,这样经过冲突后,通过回滚来解决问题。这个方式相对来说要更加优雅,也更加高效,但是它的实现机制是非常复杂的。这个很多人也能理解,就是悲观锁的设置一定比乐观锁要简单。
而像这样的一个Task co-scheduling问题,在kubernetes里,就直接通过Pod这样一个概念去解决了。因为在Kubernetes里,这样的一个App容器和LogCollector容器一定是属于一个Pod的,它们在调度时必然是以一个Pod为单位进行调度,所以这个问题根本不存在。
再次理解Pod
首先Pod里面的容器是“超亲密关系”。
这个“超”字需要理解,正常来说,有一种关系叫做亲密关系,这个亲密关系是一定可以通过调度来解决的。
比如说现在有两个Pod,他需要运行在同一个宿主机上,那就属于亲密关系,调度器一个是可以帮助去做的。但是对于超亲密关系来说,有一个问题,即它必须通过Pod来解决。因为如果超亲密关系赋予不了,那么整个Pod或者说是整个应用都无法启动。
什么叫超亲密关系呢?
比如说两个进程之间会发生文件交换,前面提到的例子就是这样,一个写日志,一个读日志;
两个进程之间需要通过localhost或者说是本地的Socket去进行通信,这种本地通信也是超亲密关系;
这两个容器或者是微服务之间,需要发生非常频繁的RPC调用,出于性能的考虑,也希望它们是超亲密关系;
两个容器或者应用,它们需要共享某些Linux Namespace。最简单常见的一个例子,就是我有一个容器需要加入另一个容器的Network Namespace。
这样我就能看到另一个容器的网络设备,和它的网络信息。
这样我就能看到另一个容器的网络设备,和它的网络信息。
像以上几种关系都属于超亲密关系,它们都是在 Kubernetes 中会通过 Pod 的概念去解决的。
现在我们理解了 Pod 这样的概念设计,理解了为什么需要 Pod。它解决了两个问题:
1. 我们怎么去描述超亲密关系;
2. 我们怎么去对超亲密关系的容器或者说是业务去做统一调度,这是Pod最主要的一个诉求。
Pod的实现机制
Pod要解决的问题
像Pod这样一个东西,本身就是一个逻辑概念。那在机器上,它究竟是怎么实现的呢?
既然说Pod要解决这个问题,核心就在于如何让一个Pod里的多个容器之间最高效的共享某些资源和数据。
因为容器之间原本是被Linux Namespace和cgroups隔开的,所以现在实际要解决的是怎么去打破这个隔离,然后共享某些事情和某些信息。这就是Pod的设计要解决的核心问题所在。
所以说具体的解法分为两部分:网络和存储
共享网络
看一个例子:比如说现在有一个Pod,其中包含了一个容器A和容器B,它们两个就要共享Network Namespace。
在Kubernetes里的解法是这样的:它会在每个Pod里,额外起一个Infra container小容器来共享整个Pod的Network Namespace。
在Kubernetes里的解法是这样的:它会在每个Pod里,额外起一个Infra container小容器来共享整个Pod的Network Namespace。
Infra container是一个非常小的镜像,大概100~200KB左右,是一个汇编语言写的、永远处于“暂停”状态的容器。由于有了这样一个Infra container之后,其他所有容器都会通过Join Namespace的方式加入到Infra container的Network Namespace中。
所以说一个Pod里面的所有容器,它们看到的网络视图是完全一样的。即:它们看到的网络设备、IP地址、Mac地址等等,跟网络相关的信息,其实全是一份,这一份都来自于Pod第一次创建的这个Infra container。这就是Pod解决网络共享的一个解法。
在Pod里面,一定有一个IP地址,是这个Pod的Network Namespace对应的地址,也是这个Infra container的IP地址。所以大家看到的都是一份,而其他所有网络资源,都是一个Pod一份,并且被Pod中的所有容器共享。这就是Pod的网络实现方式。
由于需要一个相当于说中间的容器存在,所以整个Pod里面,必然是Infra container第一个启动。并且整个Pod的生命周期是等同于Infra container的生命周期的,与容器A和B是无关的。这也是为什么在Kubernetes里面,它是允许去单独更新Pod里的某一个镜像的,即:做这个操作,整个Pod不会重建,也不会重启,这是非常重要的一个设计。
共享存储
比如说现在有两个容器,一个是Nginx,另外一个是非常普通的容器,在Nginx里面放一些文件,让我能通过Nginx访问到。所以它需要share这个目录。我share文件或者是share目录在Pod里面是非常简单的,实际上就是把volume变成Pod level。然后所有容器,就是所有同属于一个Pod的容器,他们共享所有的volume。
比如说上图这个例子,这个volume叫做shared-data,它是属于Pod level的,所以在每一个容器里可以直接声明:要挂载shared-data这个volume,只要你声明了你挂载这个volume,你在容器里去看这个目录,实际上大家看到的的就是同一份。这个就是Kubernetes通过Pod来给容器共享存储的一个做法。
所以在之前的例子中,应用容器App写了日志,只要这个日志是写在一个volume中,只要声明挂载了体样的volume,
这个volume就可以立即被另外一个LogCollector容器给看到。以上就是Pod实现存储的方式。
这个volume就可以立即被另外一个LogCollector容器给看到。以上就是Pod实现存储的方式。
详解容器设计模式
举例
比如说我现在有一个非常常见的一个诉求:我现在要发布一个应用,这个应用是JAVA写的,
有一个WAR包需要把它放到Tomcat的web APP目录下面,这样就可以把它启动起来了。
可是像这样一个WAR包或者Tomcat这样的容器的话,怎么去做,怎么去发布?
有一个WAR包需要把它放到Tomcat的web APP目录下面,这样就可以把它启动起来了。
可是像这样一个WAR包或者Tomcat这样的容器的话,怎么去做,怎么去发布?
第一种方式:可以把WAR包和Tomcat打包放进一个镜像里面。但是这样带来一个问题,就是现在这个镜像实际上揉进了两个东西。那么接下来,无论是我要更新WAR包还是说我要更新Tomcat,都要重新做一个新的镜像,这是比较麻烦的;
第二种:就是镜像里面只打包Tomcat。它就是一个Tomcat,但是需要使用数据卷的方式,比如说hostPath,从宿主机上把WAR包挂载进我们Tomcat容器中,挂到我的web APP目录下面,这样把这个容器启用起来之后,里面就能用了。
但是这时会发现一个问题:这种做法一定需要维护一套分布式存储系统。因为这个容器可能第一次启动是在宿主机A上面,
第二次重新启动就可能跑到B上去了,容器它是一个可迁移的东西,它的状态是不保持的。所以必须维护一套分布式存储系统,
使容器不管是在A还是在B上,都可以找到这个WAR包,找到这个数据。
第二次重新启动就可能跑到B上去了,容器它是一个可迁移的东西,它的状态是不保持的。所以必须维护一套分布式存储系统,
使容器不管是在A还是在B上,都可以找到这个WAR包,找到这个数据。
注意,即使有了分布式存储系统做volume,你还需要负责维护Volume里的WAR包。比如:你需要单独写一套Kubernetes Volume插件,用来在每次Pod启动之前,把应用启动所需的WAR包下载到这个Volume里,然后才能被应用挂载使用到。
这样操作带来的复杂程度还是比较高的,且这个容器本身必须依赖于一套持久化的存储插件(用来管理Volume里的WAR包内容)。
InitContainer
像这样的组合方式,有没有更加通用的方法?哪怕在本地Kubernetes上,没有分布式存储的情况下也能玩、能用、能发布。
在Kubernetes里面,像这样的组合方式,叫做Init Container。
还是同一个例子:在上图的yaml里,首先定义一个Init Container,它只做一件事情,就是把WAR包从镜像里拷贝到一个Volume里面,它做完这个操作就退出了,所以Init Container会比用户容器先启动,并且严格按照定义顺序来依次执行。
然后,这个关键在于刚刚拷贝到的这样一个目的目录:APP目录,实际上是一个Volume。而我们前面提到,一个Pod里面的多个容器,它们是可以共享Volume的,所以现在这个Tomcat容器,只是打包了一个Tomcat镜像。但在启动的时候,要声明使用APP目录作为我的Volume,并且要把它们挂载在Web APP目录下面。
而这个时候,由于前面已经运行过了一个Init Container,已经执行完拷贝操作了,所以这个Volume里面已经存在了应用的WAR包:就是sample.war,绝对已经存在这个Volume里面了。等到第二步执行启动这个Tomcat容器的时候,去挂这个Volume,一定能在里面找到前面拷贝来的sample.war。
所以可以这样去描述:这个Pod就是一个自包含的,可以把这一个Pod在全世界任何一个Kubernetes上面都顺利启动起来。不用担心没有分布式存储、Volume不是持久化的,它一定是可以公布的。
所以这是一个通过组合两个不同角色的容器,并且按照这样一些像Init Container这样一种编排方式,统一的去打包这样一个应用,把它用Pod来去做的非常典型的一个例子。像这样的一个概念,在Kubernetes里面就是一个非常经典的容器设计模式,叫做:“Sidecar”。
Sidecar
什么是Sidecar?就是说其实在Pod里面,可以定义一些专门的容器,来执行主业务容器所需要的一些辅助工作,比如我们前面举的例子,其实就干了一件事,这个Init Container,它其实就是一个Sidecar,它只负责把镜像里的WAR包拷贝到共享目录里面,以便被Tomcat能够用起来。
其他有哪些操作呢?比如说:
1. 原本需要在容器里面执行SSH需要干的一些事情,可以写脚本、一些前置条件,其实都可以通过像Init Container或者另外像Sidecar的方式去解决;
2.当然还有一个典型的例子就是我的日志收集,日志收集本身是一个进程,是一个容器,那么就可以把它打包进Pod里面去做这个收集工作;
3. 还有一个非常重要的东西就是Debug应用,实际上现在Debug整个应用都可以在应用Pod里面再定义一个额外的小的Container,它可以去exec应用Pod的namespace;
4. 查看其它容器的工作状态,这也是它可以做的。不再需要SSH登陆到容器里面去看,只要把监控组件装到额外的小容器里面就可以了,然后把它作为一个Sidecar启动起来,跟业务容器进行协作,所以同样业务监控也都可以通过Sidecar方式去做。
这种做法一个非常明显的优势就是在于其实将辅助功能从我的业务容器解耦了,所以我就能够独立发布Sidecar容器,
并且更重要的是这个能力是可以重用的,即同样的一个监控Sidecar或者日志Sidecar,可以被全公司的人共用的。
并且更重要的是这个能力是可以重用的,即同样的一个监控Sidecar或者日志Sidecar,可以被全公司的人共用的。
应用与日志收集
前面提到的应用日志收集,业务容器将日志写在一个Volume里面,而由于Volume在Pod里面是被共享的,
所以日志容器---即Sidecar容器一定可以通过共享改Volume,直接把日志文件读出来,然后存到远程存储里面,
或者转发到另外一个例子。现在业界常用的Fluentd日志进程或者日志组件,基本上都是这样的工作方式。
所以日志容器---即Sidecar容器一定可以通过共享改Volume,直接把日志文件读出来,然后存到远程存储里面,
或者转发到另外一个例子。现在业界常用的Fluentd日志进程或者日志组件,基本上都是这样的工作方式。
代理容器(Proxy)
假如现在有个Pod需要访问一个外部系统,或者一些外部服务,但是这些外部系统是一个集群,那么这个时候如何通过一个统一的、简单的方式,用一个IP地址,就把这些集群都访问到呢?有一种方法就是:修改代码。因为代码里记录了这些集群的地址;另外还有一种解耦方法,即通过Sidecar代理容器。
简单来说,单独写一个这么小的Proxy,用来处理对接外部的服务集群,它对外暴露出来的只有一个IP地址就可以了。所以接下来,业务容器主要访问Proxy,然后由Proxy去连接这些服务集群,这里的关键在于Pod里面多个容器是通过localhost直接通信的,因为它们它属于一个network Namespace,网络视图都一样,所以它们俩通信localhost,并没有性能损耗。
所以说代理容器除了做了解耦之外,并不会降低性能,更重要的是,像这样一个代理容器的代码就又可以被全公司重用了。
适配器容器(Adapter)
现在业务暴露出来的API,比如说有个API的一个格式是A,但是现在有一个外部系统要去访问我的业务容器,它只知道的一种格式是API B,所以要做一个工作,就是把业务容器怎么想办法改掉,要去改业务代码。但实际上,你可以通过一个Adapter帮你来做这层转换。
现在有个例子:现在业务容器暴露出来的监控接口是/metrics,访问这个容器的metrics的这个URL就可以拿到了。可是现在,这个监控系统升级了,它访问的URL是/health,我只认得暴露出health健康检测的URL,才能去做监控,metrics不认识。那这个怎么办?那就需要改代码了,但可以不去改代码,而是额外写一个Adapter,用来把所有对health的这个请求转发给metrics就可以了,所以这个Adapter对外暴露的是health这样一个监控的URL,这就可以了,你的业务就又可以工作了。
这样的关键还在于Pod之中的容器是通过localhost直接通信的,所以没有性能损耗,并且这样一个Adapter容器可以被全公司重用起来。
应用编排与管理:核心原理
资源元信息
Kubernetes资源对象
首先,我们来回顾一下Kubernetes的资源对象组成:注要包括了Spec、Status两部分。其中Spec部分用来描述期望的状态,Status部分用来描述观测到的状态。
现在我们来了解一下K8s的另外一个部分,即元数据部分。该部分主要包括了用来识别资源的标签:Labels,用来描述资源的注解:Annotation,用来描述多个资源之间的相互关系的OwnerReference。这些元数据在K8s运行中有非常重要的重要作用。
Labels
第一个元数据,也是非常重要的一个元数据是:资源标签。
资源标签是一种具有标识型的Key:Value元数据,这里展示了几个常见的标签。
资源标签是一种具有标识型的Key:Value元数据,这里展示了几个常见的标签。
前三个标签都打在了Pod对象上,分别标识了对应的应用环境、发布的成熟度和应用的版本。从应用标签的例子可以看到,标签的名字包括了一个域名的前缀,用来描述打标签的系统和工具,最后一个标签打在了Node对象上,还在域名前增加了版本的标识beta字符串。
标签主要用来筛选资源和组合资源,可以使用类似于SQL查询select,来根据Label查询相关资源。
Selector
最常见的Selector就是相等型Selector。
假设系统中有四个Pod,每个Pod都有标识系统层级和环境的标签,我们通过Tie:front这个标签,
可以匹配左边栏的Pod,相等型Selector还可以包括多个相等条件,多个相等条件之间是逻辑“与”的关系。
可以匹配左边栏的Pod,相等型Selector还可以包括多个相等条件,多个相等条件之间是逻辑“与”的关系。
在刚才的例子中,通过Tie=front,Env=dev的Selector,我们可以筛选出所有的Tie=front,而且Env=dev的Pod,也就是上图中左下角的Pod。另外一种Selector是集合型Selector,在例子中,Selector筛选所有环境是test或者是gray的Pod。
除了in的集合操作外,还有notion集合操作,比如tie notion(front,back),将会筛选所有tie不是front且不是back的Pod。另外,也可以根据是否存在某个label的筛选,如:Selector release,筛选所有带release标签的Pod。集合型和相等型的Selector,也可以用“,”来连接,同样的标识逻辑“与”的关系。
Annotations
一般是系统或者工具用来存储资源的非标示性信息,可以用来扩展资源的spec/status的描述。
第一个例子,存储了阿里云负载器的证书ID,我们可以看到annotations一样可以拥有域名的前缀,标注中也可以包含版本信息。
第二个annotation存储了nginx接入层的的配置信息,我们可以看到annotation中包括“,”这样无法出现在label中的特殊字符。
第三个annotations一般可以在Kubectl apply命令行操作后的资源中看到,annotation值是一个结构化的数据,
实际上是一个json串,标记了上一次kubectl操作的资源的json的描述。
实际上是一个json串,标记了上一次kubectl操作的资源的json的描述。
OwnerReference
所谓所有者,一般就是指集合类的资源,比如说Pod集合,就有replication、statefulset。
集合类资源的控制器会创建对应的归属资源。比如:replicaset控制器在操作中会创建Pod,被创建Pod的Ownerreference就指向了创建Pod的replicaset,Ownerreference使得用户可以方便地查找一个创建资源的对象,另外,还可以用来实现级联删除的效果。
操作演示
去网页查看:https://edu.aliyun.com/lesson_1651_18353?spm=5176.10731542.0.0.598120bebTC4KI#_18353
控制器模式
控制循环
控制模式最核心的就是控制循环的概念。在控制循环中包括了控制器,
被控制的系统,以及能够观测系统的传感器,三个逻辑组件。
被控制的系统,以及能够观测系统的传感器,三个逻辑组件。
当然这些组件都是逻辑的,外界通过修改资源spec来控制资源,控制器比较资源spec和status,从而计算一个diff,
diff最后会用来决定执行对系统进行什么样的控制操作,控制操作会使得系统产生新的输出,并被传感器以资源status形式上报,
控制器的各个组件将都会是独立自主地运行,不断使系统向spec表示终态趋近。
diff最后会用来决定执行对系统进行什么样的控制操作,控制操作会使得系统产生新的输出,并被传感器以资源status形式上报,
控制器的各个组件将都会是独立自主地运行,不断使系统向spec表示终态趋近。
Sensor
控制循环中逻辑的传感器主要由Reflector、Informer、Indexer三个组件构成。
Reflector通过List和Watch K8s server来获取资源的数据。List用来在Controller重启以及Watch中断的情况下,进行系统资源的全量更新;而Watch则在多次List之间进行增量的资源更新;Reflector在获取新的资源数据后,会在Delta队列中塞入一个包括资源对象信息本身以及资源对象事件类型的Delta记录,Delta队列中可以保证同一个对象在队列中仅有一条记录,从而避免Reflector重新List和Watch的时候产生产生重复的记录。
Informer组件不断地从Delta队列中弹出delta记录,然后把资源对象交给indexer,让indexer把资源记录在一个缓存中,缓存在默认设置下是用资源的命名空间来做索引的,并且可以被Controller Manager或多个Controller所共享。之后,再把这个事件交给事件的回调函数。
控制循环中的控制器组件主要由事件处理函数以及worker组成,事件处理函数之间会互相关注资源的新增、更新、删除的事件,并根据控制器的逻辑去决定是否需要处理。对需要处理的事件,会把事件关联资源的命名空间以及名字塞入一个工作队列中,并且由后续的worker池中的一个worker来处理,工作队列会对存储的对象进行去重,从而避免多个Worker处理同一个资源的情况。
Worker在处理资源对象时,一般需要用资源的名字来重新获得最新的资源数据,用来创建或者更新资源对象,或者调用其它的外部服务,Worker如果处理失败的时候,一般情况下会把资源的名字重新加入到工作队列中,从而方便之后进行重试。
控制循环例子-扩容
ReplicaSet是一个用来描述无状态应用的扩缩容行为的资源,ReplicaSet controller通过监听ReplicaSet资源来维持应用希望的状态数量,ReplicaSet中通过selector来匹配所关联的Pod,在这里考虑ReplicaSet reA的,replicas从2被改到3的场景。
首先,Refletor会watch到ReplicaSet和Pod两种资源的变化,为什么我们还会watch pod资源的变化稍后讲解。发现ReplicaSet发生变化后,在delta队列中塞入了对象是rsA,而且类型是更新的记录。
Informer一方面把新的ReplicaSet更新到缓存中,并与Namespace nsA作为索引。另一方面,调用Update的回调函数,ReplicaSet控制器发现ReplicaSet发生变化后会把字符串的nsA/rsA字符串塞入到工作队列中,工作队列后的一个Worker从工作队列中取到了nsA/rsA这个字符串的key,并且从缓存中取到了最新的ReplicaSet数据。
Worker通过比较ReplicaSet中spec和status里的数值,发现需要对这个ReplicaSet进行扩容,因此ReplicaSet的Worker创建了一个Pod,这个Pod中的Ownereference取向了ReplicaSet rsA。
然后Reflector Watch到的Pod新增事件,在delta队列中额外加入了Add类型的data记录,一方面把新的Pod记录通过Indexer存储到了缓存中,另一方面调用了ReplicaSet控制器的Add回调函数,Add回调函数通过检查pod ownerReferences找到了对应的ReplicaSet,并把包括ReplicaSet命名空间和字符串塞入到了工作队列中。
ReplicaSet的Worker在得到新的工作项之后,从缓存中取到了新的ReplicaSet记录,并得到了其所有创建的Pod,因为ReplicaSet的状态不是最新的,也就是所有创建Pod的数量不是最新的。因此在此时ReplicaSet更新status使得spec和status达成一致。
控制器模式总结
两种API设计方法
Kubernetes控制器模式依赖声明式的API。另外一种常见的API类型是命令式API。为什么Kubernetes采用声明式API,而不是命令式API来设计整个控制器呢?
首先,比较两种API在交互行为上的差别。在生活中,常见的命令式的交互方式是家长和孩子交流方式,因为孩子欠缺目标意识,无法理解家长期望,家长往往通过一些命令,教孩子一些明确的动作,比如:吃饭、睡觉类似的命令。我们在容器编排体系中,命令式API就是通过向系统发出明确的操作来执行的。
而常见的声明式交互方式,就是老板对自己员工的交流方式。老板一般不会给自己的员工下很明确的决定,实际上可能老板对于要操作的事情本身,还不如员工清楚。因此,老板通过给员工设置可量化的业务目标的方式,来发挥员工自身的主观能动性。比如说,老板会要求某个产品的市场占有率达到80%,而不会指出要达到这个市场占有率,要做的具体操作细节。
类似的,在容器编排体系中,我们可以执行一个应用实例副本数保持在3个,而不用明确的去扩容Pod或是删除已有的Pod,来保证副本数在三个。
命令式API的问题
命令式API最大的一个问题在于错误处理
在大规模的分布式系统中,错误是无处不在的。一旦发出的命令没有响应,
调用方只能通过反复重试的方式来试图恢复错误,然而盲目的重试可能会带来更大的问题。
调用方只能通过反复重试的方式来试图恢复错误,然而盲目的重试可能会带来更大的问题。
假设原来的命令,后台实际上已经执行完成了,重试后又多执行了一个重试的命令操作。为了避免重试的问题,系统往往需要在执行命令前,先记录一下需要执行的命令,并且在重启等场景下,重做待执行的命令,而且在执行的过程中,还需要考虑多个命令的先后顺序、覆盖关系等等一些复杂的逻辑情况。
实际上许多命令式的交互系统后台往往还会做一个巡检的系统,
用来修正命令处理超时、重试等一些场景造成数据不一致的问题;
用来修正命令处理超时、重试等一些场景造成数据不一致的问题;
然而,因为巡检逻辑和日常操作逻辑是不一样的,往往在测试上覆盖不够,在错误处理上不够严谨,
具有很大的操作风险,因此往往很多巡检系统都是人工来触发的。
具有很大的操作风险,因此往往很多巡检系统都是人工来触发的。
最后,命令式API在处理多并发访问时,也很容易出现问题;
假如有多方并发的对一个资源请求进行操作,并且一旦其中有操作出现了错误,就需要重试。那么最后哪一个操作失效了,就很难确认,也无法保证。很多命令式系统往往在操作前会对系统进行加锁,从而保证整个系统最后失效行为的可预见性,但是加锁行为会降低整个系统的操作执行效率。
相对的,声明式API系统里天然地记录了系统现在和最终的状态。
不需要额外的操作数据。另外因为状态的幂等性,可以在任意时刻反复操作。在声明式系统运行的方式里,正常的操作实际上就是对资源状态的巡检,
不需要额外开发巡检系统,系统的运行逻辑也能够在日常的运行中得到测试和锤炼,因此整个操作的稳定性能够得到保证。
不需要额外开发巡检系统,系统的运行逻辑也能够在日常的运行中得到测试和锤炼,因此整个操作的稳定性能够得到保证。
最后,因为资源的最终状态是明确的,我们可以合并多次对状态的修改。可以不需要加锁,就支持多方的并发访问。
控制器模式总结
1. Kubernetes所采用的控制器模式,是由声明式API驱动的。确切来说,是基于对Kubernetes资源对象的修改来驱动的;
2. Kubernetes资源之后,是关注该资源的控制器。这些控制器将异步的控制系统向设置的终态趋近;
3. 这些控制器是自主运行的,使得系统的自动化和无人值守成为可能;
4. 因为Kubernetes的控制器和资源都是可以自定义的,因此可以方便的扩展控制器模式。特别是对于有状态应用,
我们往往通过自定义资源和控制器的方式,来自动化运维操作。这个也就是后续会介绍的operator的场景。
我们往往通过自定义资源和控制器的方式,来自动化运维操作。这个也就是后续会介绍的operator的场景。
章节总结:
Kubernetes资源对象中的元数据部分,主要包括了用来识别资源的标签:Labels,用来描述资源的注解;
Annotations,用来描述多个资源之间相互关系的OwnerReference。这些元数据在K8s运行中有非常重要的作用;
Annotations,用来描述多个资源之间相互关系的OwnerReference。这些元数据在K8s运行中有非常重要的作用;
控制型模式中最核心的就是控制循环的概念;
两种API设计方法:声明式API和命令式API;Kubernetes所采用的控制器模式,是由声明式API驱动的;
应用编排与管理: Deployment
需求来源
背景问题
如图所示:如果我们直接管理集群中所有的Pod,应用A、B、C的Pod,其实是散乱地分布在集群中。
现在有以下的问题:
首先,如何保证集群内可用Pod的数量?也就是说我们应用A四个Pod如果出现了一些宿主机故障,或者一些网络问题,如何能保证它可用的数量?
如何为所有Pod更新镜像版本?我们是否要某一个Pod去重建新版本的Pod?
然后在更新过程中,如何保证服务的可用性?
以及更新过程中,如果发现了问题,如何快速回滚到上一个版本?
Deployment:管理部署发布的控制器
可以看到我们通过Deployment将应用A、B、C分别规划到不同的Deployment中,
每个Deployment其实是管理的一组相同的应用Pod,这组Pod我们认为它是相同的一个副本,
那么Deployment能帮我们做什么事情呢?
每个Deployment其实是管理的一组相同的应用Pod,这组Pod我们认为它是相同的一个副本,
那么Deployment能帮我们做什么事情呢?
1. 首先,Deployment定义了一种Pod期望数量,比如说应用A,我们期望Pod数量是四个,那么这样的话,controller就会持续维持Pod数量为期望的数量。当我们与Pod出现了网络问题或者宿主机问题的话,controller能帮我们恢复,也就是新扩出来对应的Pod,来保证可用的Pod数量与期望数量一致;
2. 配置Pod发布方式,也就是说controller会按照用户给定的策略来更新Pod,
而且更新过程中,也可以设定不可用Pod数量在多少范围内;
而且更新过程中,也可以设定不可用Pod数量在多少范围内;
3. 如果更新过程中发生问题的话,即所谓“一键回滚”,也就是说你通过一条命令
或者一行修改能够将Deployment下面所有Pod更新为某一个旧版本。
或者一行修改能够将Deployment下面所有Pod更新为某一个旧版本。
用例解读
Deployment语法
上图可以看到一个最简单的Deployment的yaml文件。
"apiVersion : apps/v1",也就是说Deployment当前所属的组是apps,版本是v1。"metadata"是我们看到的Deployment元信息,也就是往期回顾中的Labels、Selector、Pod.image,这些都是往期提到的知识点。
Deployment作为K8s资源,它有自己的metadata元信息,这里我们定义的Deployment.name是nginx.Deployment。Deployment.spec中首先要有一个核心的字段,即replicas,这里定义期望的Pod数量为三个;selector其实是Pod选择器,那么所有扩容出来的Pod,它的Labels必须匹配selector层上的image.lebels,也就是app.nginx。
就如上面的Pod模板template中所述,这个template它其实包含了两部分内容:
一部分是我们期望Pod的metadata,其中包含了labels,即跟selector.matchLabels相匹配的一个Labels;
第二部分是template包含的一个Pod.spec。这里Pod.spec其实是Deployment最终创建出来Pod的时候,
它所用的Pod.spec,这里定义了一个container.nginx,它的镜像版本是nginx:1.7.9。
它所用的Pod.spec,这里定义了一个container.nginx,它的镜像版本是nginx:1.7.9。
下面是遇到的新知识点:
第一个是replicas,就是Deployment中期望的或者终态数量;
第二个是template,也就是Pod相关的一个模板。
查看Deployment状态
当我们创建出一个Deployment的时候,可以通过Kubectl get deployment,
看到Deployment总体的一个状态。如图所示:
看到Deployment总体的一个状态。如图所示:
重点讲解一下AVAILABLE:这个其实是运行过程中可用的Pod数量。后面会提到,这里AVAILABLE并不简单是可用的,
也就是Ready状态的,它其实包含了一些可用超过一定时间长度的Pod;
也就是Ready状态的,它其实包含了一些可用超过一定时间长度的Pod;
查看Pod
上图中有三个Pod,Pod名字格式我们不难看到。
最前面一段Lnginx-deployment,其实是Pod所属Deployment.name;之间一段:template-hash,
这里三个Pod是一样的,因为这三个Pod其实都是同一个template中创建出来的。
这里三个Pod是一样的,因为这三个Pod其实都是同一个template中创建出来的。
最后一段,是一个random的字符串,我们通过get.pod可以看到,Pod的ownerReference即Pod所属的controller资源,并不是Deployment,而是一个ReplicaSet。这个ReplicaSet的name,其实是nginx-deployment加上pod.template-hash,后面会提到。所有的Pod都是ReplicaSer创建出来的,而ReplicaSet它对应某一个具体的Deployment.template版本。
更新镜像
接下来我们可以看一下,如何对一个给定的Deployment更新它所有Pod的镜像版本呢?
这里我们可以执行一个kubectl命令:
这里我们可以执行一个kubectl命令:
kubectl set image deployment.v1.apps/nginx-deployment nginx=nginx:1.9.1
首先kubectl后面有一个set image固定写法,这里指的是设定镜像;其次是一个deployment.v1.apps,这里也是一个固定写法,写的是我们要操作的资源类型,deployment是资源名、v1是资源版本、apps是资源组,这里也可以简写为deployment或者deployment.apps,比如说写为deployment的时候,默认将使用apps组v1版本。
第三部分是要更新的deployment的name,也就是我们的nginx-deployment;再往后的nginx其实指的是template,也就是Pod中的container.name;这里我们可以注意到:一个Pod中,其实可能存在多个container,而我们指定想要更新的镜像的container.name,就是nginx。
最后,指定我们这个容器期望更新的镜像版本,这里指的是nginx: 1.9.1。如下图所示:
当执行完这条命令之后,可以看到deployment中的template.spec已经更新为nginx: 1.9.1。
当执行完这条命令之后,可以看到deployment中的template.spec已经更新为nginx: 1.9.1。
快速回滚
如果我们在发布过程中遇到了问题,也支持快速回滚。通过kubectl执行的话,其实是"kubectl rollout undo"这个命令,
可以回滚到Deployment上一个版本;通过"rollout undo"加上"to-revision"来指定可以回滚到某一个具体的版本。
可以回滚到Deployment上一个版本;通过"rollout undo"加上"to-revision"来指定可以回滚到某一个具体的版本。
DeploymentStatus
最后我们来看一下DeploymentStatus。前面我们学习到,每一个资源都有它的spec.Status。这里可以看一下,
deploymentStatus中描述的三个其实是它的conversion状态,也就是Processing、Complete以及Failed。
deploymentStatus中描述的三个其实是它的conversion状态,也就是Processing、Complete以及Failed。
以Processing为例:Processing指的是Deployment正在处于扩容和发布中。比如说Processing状态的deployment,它所有的replicas及Pod副本全部达到最新版本,而且是available,这样的话,就可以进入complete状态。而complete状态如果发生了一些扩缩容的话,也会进入Processing这个处理工作状态。
如果在处理过程中遇到一些问题:比如说拉取镜像失败了,或者说readiness probe检查失败了,就会进入failed状态;如果在运行过程中即complete状态,中间运行时发生了一些pod readiness probe检查失败,这个时候deployment也会进入failed状态。进入failed状态之后,除非所有点replicas均变成available,而且是update最新版本,deployment才会重新进入complete状态。
操作演示
Deployment创建及状态
这里连接一个阿里云服务集群。我们可以看到当前集群已经有几个可用的node
首先创建对应的deployment。可以看到deployment中的desired(渴望的)、current(当前的)、
up-to-date(到达最新的期望版本的Pod数量)以及available已经都达到了可用的期望状态。
up-to-date(到达最新的期望版本的Pod数量)以及available已经都达到了可用的期望状态。
Deployment的结果
这里看到spec中replicas是三个,selector以及template labels中定义的标签都是app: nginx,
spec中的image是我们期望的nginx: 1.7.9;status中的available.replicas,readReplicas以及updatedReplicas都是3个。
spec中的image是我们期望的nginx: 1.7.9;status中的available.replicas,readReplicas以及updatedReplicas都是3个。
Pod状态
可以看到:Pod中ownerReferences的功能是ReplicaSet;pod.spec.container里的镜像是1.7.9。
这个Pod已经是Running状态,而且它的condition.status是“true”,表示它的服务已经可用了。
这个Pod已经是Running状态,而且它的condition.status是“true”,表示它的服务已经可用了。
更新升级
当前只有最新版本的replicaset,那么现在尝试对deployment做一次升级。
“kubectl set image”这个操作命令,后面接“deployment”,
加deployment.name,最后指定容器名,以及我们期望升级的镜像版本。
加deployment.name,最后指定容器名,以及我们期望升级的镜像版本。
接下来我们看下deployment中的template中的image已经更新为1.9.1。
这个时候我们再来get pod看一下状态,三个pod已经升级为新版本,pod名字中的pod-template-hash也已更新。
可以看到:旧版本replicaset的spec数量以及pod数量都是0,新版本的pod数量是3个。
假设又做了一次更新,这个时候get pod其实可以看到:当前的pod其实是有两个旧版本的处于running,
另一个旧版本是在删除中;而两个新版本的pod,一个已经进入running,一个还在creating中。
另一个旧版本是在删除中;而两个新版本的pod,一个已经进入running,一个还在creating中。
这时我们可用的pod数量即非删除状态的pod数量,其实是4个,已经超过了replica原先在deployment设置的数量3个。
这个原因是我们在deployment中有maxavailable和maxsugar两个操作,这两个配置可以限制我们在发布过程中的一些策略。
这个原因是我们在deployment中有maxavailable和maxsugar两个操作,这两个配置可以限制我们在发布过程中的一些策略。
历史版本保留 revisionHistoryLimit
上图我们可以看到最新版本的replicaset是3个pod,另外还有两个历史版本的replicaset,那么会不会存在一种情况:就是随着deployment持续更新,这个旧版本的replicaset会越积越多呢?其实deployment提供了一个机制来避免这个问题:在deployment spec中,有一个revisionHistoryLimit,它默认值是10,它其实保证了保留历史版本的replicaset的数量,我们尝试把它改为1.
由上面两张图,可以看到两个replicaset,也就是说,除了当前版本的replicaset之外,
旧版本的replicaset其实只保留了一个。
旧版本的replicaset其实只保留了一个。
回滚
最后,我们尝试做一下回滚。首先再来看replicaset,这时发现旧版本的replicaset数量从0增加到了2个,而新版本的replicaset数量从3个削减为1个,
表示它已经开始在做回滚的操作。然后再观察一下,旧版本的数量已经是3个,即已经回滚成功,而新版本的pod数量变为0个。
表示它已经开始在做回滚的操作。然后再观察一下,旧版本的数量已经是3个,即已经回滚成功,而新版本的pod数量变为0个。
我们最后再get pod看一下:
这时,3个pod.template-hash已经更新为旧版本的hash,但其实这3个pod都是重新创建出来的,而并非我们在前一个版本中创建的3个pod。
换句话说,也就是我们回滚的时候,其实是创建了3个旧版本的pod,而并非把先前的3个pod找回来。
换句话说,也就是我们回滚的时候,其实是创建了3个旧版本的pod,而并非把先前的3个pod找回来。
架构设计
管理模式
我们来看一下架构设计。首先简单看一下管理模式:Deployment只负责管理管理不同版本的ReplicaSet,
由ReplicaSet来管理具体的Pod副本数,每个ReplicaSet对应Deployment template的一个版本。
在上文的例子中可以看到,每一次修改template,都会生成新的ReplicaSet,这个ReplicaSet底下的Pod其实都是相同的版本。
由ReplicaSet来管理具体的Pod副本数,每个ReplicaSet对应Deployment template的一个版本。
在上文的例子中可以看到,每一次修改template,都会生成新的ReplicaSet,这个ReplicaSet底下的Pod其实都是相同的版本。
如上图所示:Deployment创建ReplicaSet,而ReplicaSet创建Pod。他们的OwnerRef其实都对应了其控制器的资源。
Deployment控制器
首先,我们所有的控制器都是通过Informer中的Event做一些Handler和Watch。这个地方Deployment控制器,其实是关注Deployment和ReplicaSet 中的Event,收到事件后会加入到队列中。而Deployment controller从队列中取出来之后,它的逻辑会判断Check Paused,这个Paused其实是Deployment是否需要新的发布,如果Paused设置为true的话,就表示这个Deployment只会做一个数量上的维持,不会做新的发布。
如上图,可以看到如果Check Paused为Yes也就是true的话,那么只会做Sync replicas。也就是说把replicas sync同步到对应
的ReplicaSet中,最后再update Deployment status,那么controller这一次的ReplicaSet就结束了。
的ReplicaSet中,最后再update Deployment status,那么controller这一次的ReplicaSet就结束了。
那么如果paused为false的话,它就会做Rollout,也就是通过Create或者是Rolling的方式来做更新,
更新的方式其实也是通过Create/Update/Delete这种ReplicaSet来做实现的。
更新的方式其实也是通过Create/Update/Delete这种ReplicaSet来做实现的。
ReplicaSet控制器
当Deployment分配ReplicaSet之后,ReplicaSet控制器本身也是从Informer中watch一些事件,这些事件包含了ReplicaSet和Pod的事件。
从队列中取出之后,ReplicaSet controller的逻辑很简单,就只管理副本数。也就是说如果controller发现replicas比Pod数量大的话,
就会扩容,而如果发现实际数量超过期望数量的话,就会删除Pod。
从队列中取出之后,ReplicaSet controller的逻辑很简单,就只管理副本数。也就是说如果controller发现replicas比Pod数量大的话,
就会扩容,而如果发现实际数量超过期望数量的话,就会删除Pod。
上面Deployment控制器的图中可以看到,Deployment控制器其实做了更复杂的事情,
包含了版本控制,而它把每一个版本下的数量维持工作交给了ReplicaSet来做。
包含了版本控制,而它把每一个版本下的数量维持工作交给了ReplicaSet来做。
扩/缩容模拟
下面看一些操作模拟,比如说扩容模拟。这里有一个Deployment,它的副本数是2,对应的ReplicaSet有pod1、pod2。这时如果我们修改Deployment replicas,controller就会把replicas同步到当前版本的ReplicaSet中,这个ReplicaSet发现当前有2个Pod,不满足当前期望的3个,就会创建一个新的Pod3。
发布模拟
发布的情况会稍微复杂一点。这里可以看到Deployment当前初始的template,比如说template1这个版本。template1这个ReplicaSet对应的版本下有三个Pod:Pod1,Pod2,Pod3。
这时修改template中一个容器的image,Deployment controller就会新建一个对应template2的ReplicaSet。创建出来之后ReplicaSet会逐渐修改两个ReplicaSet的数量,比如它会逐渐增加ReplicaSet2中的replicas的期望数量,而逐渐减少ReplicaSet1中的Pod数量。
那么最终达到的效果是:新版本的Pod为Pod4、Pod5和Pod6,旧版本的Pod已经被删除了,这里就完成了一次发布。
回滚模拟
根据上面的发布模拟可以知道Pod4、Pod5和Pod6已经发布完成。这时发现当前的业务版本是有问题的,如果做回滚的话,
不管是通过rollout命令还是通过回滚修改template,它其实都是把template回滚为旧版本的template1。
不管是通过rollout命令还是通过回滚修改template,它其实都是把template回滚为旧版本的template1。
这个时候Deployment会重新修改ReplicaSet1中的Pod的期望数量,把期望数量修改为3个,
且会逐渐减少新版本也就是ReplicaSet2中的replicas数量,最终的效果就是把Pod从旧版本重新创建出来。
且会逐渐减少新版本也就是ReplicaSet2中的replicas数量,最终的效果就是把Pod从旧版本重新创建出来。
发布模拟的图中可以看到,其实初始版本中Pod1、Pod2、Pod3是旧版本,而回滚之后其实是Pod7、Pod8、Pod9。就是说它回滚并不是把之前的Pod重新找回来,而是说重新创建出符合旧版本template的Pod.
spec字段解析
首先看一下Deployment中其它的spec字段:
MinReadySeconds
Deployment会根据Pod ready来看Pod是否可用,但是如果我们设置了MinReadySeconds之后,比如设置为30秒,那么Deployment就一定会等到Pod ready超过30秒之后才会认为Pod是available的。Pod available的前提条件是Pod ready,但是ready的Pod不一定是available的,它一定要超过MinReadySeconds之后,才会判断为available;
revisionHistoryLimit
保留revision,即保留历史ReplicaSet的数量,默认值是10个。这里可以设置为一个或者两个,
如果回滚可能性比较大的话,可以设置数量超过10;
如果回滚可能性比较大的话,可以设置数量超过10;
paused
paused是标识,Deployment只做数量维持,不做新的发布,这里在Debug场景可能会用到;
progressDeadineSeconds
前面提到当Deployment处于扩容或者发布状态时,它的condition会处于一个processing的状态,processing可以设置一个超时时间。
如果超过超时时间还处于processing,那么controller将认为这个Pod会进入failed的状态。
如果超过超时时间还处于processing,那么controller将认为这个Pod会进入failed的状态。
升级策略字段解析
Deployment在RollingUpdate中主要提供了两个策略,一个是MaxUnavalable,另一个是MaxSurge。
这两个字段解析的意思,可以看下图中详细的comment,或者简单解释一下:
这两个字段解析的意思,可以看下图中详细的comment,或者简单解释一下:
MaxUnavailable
滚动过程中最多有多少个Pod不可用;
MaxSurge
滚动过程中最多存在多少个Pod超过预期replicas数量。
上文中提到,ReplicaSet为3的Deployment在发布的时候可能存在一种情况:新版本的ReplicaSet和旧版本的ReplicaSet都可能有两个replicas,加在一起就是4个,超过了我们期望的数量三个。这是因为我们默认的MaxUnavailable和MaxSurge都是25%,默认Deployment在发布的过程中,可能有25%的replica是不可用的,也可能超过replica数量25%是可用的,最高可以达到125%的replica数量。
这里其实可以根据用户实际场景来做设置。比如当用户的资源足够,且更注意发布过程中的可用性,可设置MaxUnavailable较小、MaxSurge较大。但如果用户的资源比较紧张,可以设置MaxSurge较小,甚至设置为0,这里要注意的是MaxSurge和MaxUnavailable不能同时设置为0。
理由不难理解,当MaxSurge为0的时候,必须要删除Pod,才能扩容Pod;如果不删除Pod是不能新扩Pod的,因为新扩出来的话,总共的Pod数量就会超过期望数量。而两者同时为0的话,MaxSurge保证不能新扩Pod,而MaxUnavailable不能保证ReplicaSet中有Pod是available的,这样就会产生问题。属于说这两个值不能同时为0.用户可以根据自己的实际场景来设置对应的、合适的值。
简单总结
Deployment是Kubernetes中常见的一种Workload,支出部署管理多版本的Pod;
Deployment管理多版本的方式,是针对每个版本的template创建一个ReplicaSet,由ReplicaSet维护
一定数量的Pod副本,而Deployment只需要关心不同版本的ReplicaSet里要指定多少数量的Pod;
一定数量的Pod副本,而Deployment只需要关心不同版本的ReplicaSet里要指定多少数量的Pod;
因此,Deployment发布部署的根本原理,就是Deployment调整不同版本ReplicaSet里的终态副本数,
以此来达到多版本Pod的升级和回滚。
以此来达到多版本Pod的升级和回滚。
0 条评论
下一页