极客大学架构师训练营 性能优化 进程 线程 锁 存储 分布式数据库 第14课 听课总结
说明
讲师:李智慧
操作系统
架构师要头脑中要实时运行操作系统的架构,一看到指标有异常,就要知道可能哪些地方有问题。
程序运行时架构
程序是静态的。
程序运行起来以后,被称作进程。
进程是有生命的,程序代码是静态的。
程序代码运行起来后,在堆内存空间操作数据才会有数据冲突,程序调用是在栈内存空间调用指令。

操作系统多任务运行环境
计算机的 CPU 核心数是有限的。但是,服务器可以同时处理数以百计甚至数以千计的并发用户请求。
那么,计算机如何做到的?
进程分时执行。
多个进程是活着的,但是不是每个进程都是同时执行的,是CPU在按照分时执行的,因为时间片比较小,所以用户感觉到都是一起执行的。就像播放视频一样,视频的原理是图片快速切换,比如1秒钟60帧,切换60张图片,感觉就是动的。
也就是多个进程存在争夺资源,比如锁,修改数据等。
进程的运行期状态
运行:当一个进程在 CPU 上运行时,则称该进程处于运行状态。处于运行状态的进程的数目小于等于 CPU 的数目。
就绪:当一个进程获得了除 CPU 以外的一切所需资源,只要得到 CPU 即可运行,则称此进程处于就绪状态,就绪状态有时候也称为等待运行状态。
阻塞:也称为等待或者睡眠状态,当一个进程正在等待某一事件发生(例如等待 I/O 完成,等待锁...)而暂时停止运行,这时即使把 CPU 分配给进程也无法运行,故称该进程处于阻塞状态。
进程 vs 线程
不同进程轮流在 CPU 上执行,每次都要进行进程间 CPU 切换,代价非常大。因此服务器应用通常是单进程多线程。
进程从操作系统获得基本的内存空间,所有的线程共享着进程的内存地址空间。而每个线程也会拥有自己私有的内存地址范围,其他线程不能访问。

线程栈
```c++
void f(){
int x = g(1);
x++; // g 函数返回,当前堆栈顶部为f函数栈帧,在当前栈帧继续执行f函数的代码.
}
int g(int x) {
return x + 1;
}

什么情况下回Stack Overflow?
1. 栈初始化小了,函数调用过多,栈就满了。
2. 无限递归,没有出口函数,一定会把栈打满了。
## Java Web 应用多线程运行时视图
Tomcat 启动的时候做了什么? Tomcat启动JVM进程,JVM进程启动Tomcat进程,用下面的指令。
```java
java org.apache.catalina.startup.Bootstrap "$@" start
Tomcat启动以后,还启动了一堆线程,也就是开辟一个线程池。当有一个用户请求,则分配一个线程。线程调用应用程序,比如servelet响应request,response。
线程做的事情,就是执行代码。方法执行就是栈中的指令。在不同的栈帧中,线程是互不影响的。也就是线程内的临时变量,是安全的。这也是推荐无状态编程的原因。

线程安全
在某些代码修改内存堆(进程共享内存)里的数据的时候,如果多个线程在同时执行,就可能会出现同事修改数据的情况,比如,两个线程同时对同一个堆中的数据执行 +1 操作,最终数据只会被加一次,这就是人们常说的线程安全问题,实际上线程的结构应该是依次 +1, 即最终结果应该是 +2.

临界区
多个线程访问共享资源的这段代码被称为临界区,解决线程安全问题的主要方法是使用锁,将临界区的代码加锁,只有获得锁的线程才能执行临界区代码。
lock.lock(); // 线程获得锁
i++; // 临界区代码,i位位于堆中
lock.unlock(); // 线程释放锁
阻塞导致高并发系统崩溃
锁(IO)会引起线程阻塞。阻塞导致线程既不能继续执行,也不能释放资源。进而导致资源耗尽,最终导致系统崩溃。

瓶颈有两个:
避免阻塞引起的崩溃
限流:控制进入计算机的请求数,进而减少创建的线程数。
降级:关闭部分功能程序的执行,尽早释放线程。
反应式:异步;无临界区(Actor模型)
锁
锁原语 CAS (Compare-And-Swap)
CAS(V, E, N)
V 表示更新的变量
E 表示预期值
N 表示新值
如果V值等于E值,则将V的值设为N,若V值和E值不同,什么都不做。
CAS是一种系统原语,原语的执行必须是连续的,在执行过程中不允许被中断。
Java 通过 CAS原语在对象头中修改Mark Work实现加锁


偏向锁 轻量级锁 重量级锁
偏向锁:指一段同步代码一直被一个线程访问,那么该线程会自动获取锁,降低获取锁的代价。
轻量级锁:指当锁是偏向锁时,被另一个线程所访问,偏向锁就会升级为轻量级锁,其它线程会通过自旋的形式尝试获取锁,不会阻塞,提供性能。
重量级锁:指当锁是轻量级锁时,另一个线程虽然自旋,但自旋不会一直持续下去,当自旋到一定次数时,还没获取到锁,就会进入阻塞,该锁膨胀为重量级锁,重量级锁会让其它申请的线程进入阻塞,性能降低。
多 CPU 情况下的锁

总线锁 与 缓存锁
总线锁:使用处理器的 信号,当一个处理器在内存总线上输出此信号的时候,其它处理器的请求将被阻塞,该处理器独占内存。
缓存锁:是指内存区域如果被缓存在处理器的缓存行中,并且在 Lock 操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声言 信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其它处理器回写已被锁定的缓存行数据时,会是缓存行无效。
公平锁 与 非公平锁
公平锁就是多个线程按照申请锁的顺序来获取锁的。
非公平锁就是多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,可能会造成饥饿现象。
轻量级锁 是非公平锁,靠自旋,等待 CPU 调度。
重量级锁 是公平锁,顺序排队,先进先出。
可重入锁
可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。
独享锁/互斥锁 共享锁 读写锁
独享锁/互斥锁:该锁一次只能被一个线程所持有。
共享锁:该锁可以被多个线程所持有。
读写锁:多个线程之间并不互斥,而写线程则要求与任何线程互斥。
乐观锁 悲观锁
悲观锁认为对于同一数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,检查是否已经被修改过,如果修改过,就放弃。
总线锁:是悲观锁
缓存锁:是乐观锁
分段锁 (锁的一种应用)
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组的一段进行加锁操作。
是通过分段锁的形式来实现高效并发操作的。
自旋锁
自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗 CPU。
轻量级锁是自旋锁。
异步并发分布式编程框架akka

Akka's vision
Simpler concurrency (scale up)
Simpler distribution (scale out)
Simple fault-tolerance (self healing)
All of that with a single unified programming model

Scale up

The Akka toolkit
* Akka runs on the JVM
* Akka can be used from Java and Scala
* Akka can be integrated with common infrastructure, e.g. Spring, etc.
Core Concept: Actor
Carl Hewitt (1973): Fundamental unit of computation.
* Behavior - react on messages it receives.
* State - shielded from the rest of the world, no need for synchronization.
* Communication - interact with other actors exclusively via messages.
The Akka Actor

Throughput on a single box

这个机器有90多个CPU, 从图中可以看出。
Receive message
One at a time
public class Hello extends UntypedActor {
@Override
public void onReceive(Object message) {
System.out.println("Hello, world!");
}
}
Send message
Asynchronous and nonblocking
ActorRef hello = ...
hello.tell("Hi!");
ActorRef path
Local:
Remote
The Akka ActorSystem

ActorSystem 比较像公司模型,公司CEO要开发一个产品,把想法给总监,总监看一下目前有多少个团队,把任务分配给团队处理。CEO就去马尔代夫去旅游去了。组里把活干完了,组长层层通知到上级,最后通知总监,总监开始推广到市场。最后把结果通知给CEO。
Router for cluster
val router = system.actorOf(
Props[SomeActor].withRouter(
RoundRobinRouter(nrOfInstances=5)))
Embrace failure
Let it crash!
Supervision: Like in real life, parents supervise their children (manage children's failures)
Depending on the parent's supervisor strategy, failing actors can get stopped, restarted or resumed.
private SupervisorStrategy strategy = new OneForOneStrategy(10, Duration.parse("1 minute"),
new Function() {
@Override public Directive apply(Throwable t) {
if (t instanceof ArithmeticException) return resume();
else if (t instanceof NullPointerException) return restart();
else return escalate();
}
}
);
@Override public SupervisorStrategy supervisorStrategy() {
return strategy;
}
The HakkyHour bar
Our drinks: Akkarita, MaiPlay, PinaScalada.
Our actors: guests, waiters, head waiter, barkeepers.
Our messages: Order, drink served, complaint, etc.
Our failures: Guest drunk, waiter frustrated.
HakkyHour messages

Benefits of using Akka actors
You don't have to deal with concurrency details.
You can manage failures easily.
Distribution is just a deployment decision (not covered here).
akka {
actor {
deployment {
"/creationActor/*" {
remote = "akka.tcp://CalculatorWorkerSystem@127.0.0.1:2552"
}
}
}
}
Dew Architecture

文件 与 硬盘 I/O
机械硬盘

瓶颈:移动磁头的速度比较慢。
固态硬盘

B+ 树

机械硬盘用的数据机构是 B+ 树。
读取速度:。
LSM 树
优化移动磁头,就是用LSM 树,减少查找,也就是减少了移动磁头的次数。

文件控制块
文件系统将硬盘空间以块为单位进行划分,每个文件占据若干个块,然后再通过一个文件控制块 FCB 记录每个文件占据的硬盘数据块。

Linux Inode 文件控制块
inode 中记录着文件权限、所有者、修改时间和文件大小等文件属性信息,以及文件数据块硬盘地址索引。
inode 是固定结构的,能够记录的硬盘地址索引数也是固定的,只有15个索引。
每个 inode 最多可以存储 个数据块,如果每个数据块的大小为 4k,也就是单个文件最大不超过70G。

问题:
RAID 独立硬盘冗余阵列

RAID 0:
把数据分给多个硬盘同时写。比如Data这个数据,同时写到3块硬盘,Dat就同时分别写到三块硬盘,a还是写到第一块硬盘。
缺点:如果一块硬盘坏了,数据就会丢失。
一般一块硬盘半年就会坏掉。
RAID 1:
如果两块硬盘写一样的数据,那么写的速度就慢了。
RAID 10:
一个数据同时写到2块硬盘。速度和备份都满足了。但是会浪费一半硬件。
RAID 5:
P表示异或结果,如果一块硬盘坏了,可以通过前面的数据,和P(异或结果)可以算出坏掉的数据。P数据需要螺旋式的写到别的盘上。
RAID 6:
P, Q 表示不同的异或算法,以防RAID 5同时坏掉两块盘的情况。

分布式文件系统 HDFS
当要存储百T,甚至上前T的数据的时候,怎么办?就需要 HDFS上了。

元数据
文件名:
副本数:3