吞吐量(批处理,高吞吐,高延迟): 单位时间内处理的请求数,吞吐量时系统的综合指标,硬件
层面的cpu/磁盘/网络的任何一种都有可能称为瓶颈,软件层面的数据库,代码逻辑也对吞吐量
造成影响
I/O密集型应用: 非常消耗I/O资源,很少使用cpu资源,典型代表是web应用
计算密集型应用: 非常消耗cpu资源,很少使用I/O资源,典型代表是机器学习算法的训练
响应时间(实时处理,低吞吐,低延迟): 单个请求从发出请求到收到响应消耗的时间,一般使用
平均响应时间(所有用户的总响应时间 / 用户数)
并发数: 系统能同时承受的最大用户数
QPS: querys per second,反映了服务器接受请求的能力,有可能一个网页的请求发送了多个
query
TPS: trasactions per second,反映了服务器处理完整请求的能力,如一个完整网页(包括多个
请求)从请求到返回算做一个事务
以饭店为例,小王从烹饪学校学成归来,自己开了一家小饭店,
由于资金有限,一开始只有他一个人单干,招呼,点菜,做菜,端菜,结账统统一个人来,为了避免
顾客等待,他一次只服务一个顾客,当前一个顾客酒足饭饱后才开始接待第二个顾客,因此大家
觉得去他家吃一顿饭等待的时间太长了(等待时老板还不搭理你),因此客流量惨淡.小王反思后
决定提高自己的业务水平,很快他干活儿麻溜了许多,顾客的响应时间大幅降低,小店的吞吐量
也上去了,并发数增加. --> 单线程reactor server
一个月后,小王干活的速度再也提不上去了,他想了想,给一个顾客做饭时却耽误了接待下一个
顾客,白白丢了生意,因此他拉来自己的妻子帮他当服务员,负责招呼,点菜,端菜,结账等,而他
则专职做菜.这样对顾客请求的响应和处理分隔开,可以同时进行(处理前一个顾客的请求时
不影响招呼下一个顾客),这样虽然顾客从进店到吃完离开总的时间虽然没变,但是进店就能响应
自己对点菜的需求(虽然还是得等),顾客的体验变好(响应了一部分请求),因此店里的客流量
开始增加(老板的厨艺还是不错的,值得等待).顾客感受到的响应时间(只是部分请求的响应,
实际总的响应时间并无改变)变短,店里吞吐量不变,并发数增加
小王的夫妻店生意越来越好,但是顾客的最长响应时间(最后一个光临的顾客)也随之增加(假设
妻子点菜等不花时间,最长响应时间=前面等待顾客数 * 小王做菜的时间),因为小王的做菜速度
已经达到了极限.小王于是招了一个厨师,做菜的速度快了一倍,响应时间减半,吞吐量翻番,并发
数翻番 --> 工作者线程池reactor server
顾客越来越多,妻子甚至也忙不过来了,因此小王决定让妻子专门负责在前台招呼顾客,负责分配
座位,结账,另外招聘了3个服务员负责点菜,端菜,顾客的总响应时间和店里的吞吐量并无变化,
但是顾客感受到的响应时间减少,并发数增加 --> 多reactor server
总结:
常见软件系统的分类:
设计目标: 给定响应时间阈值尽可能少的使用系统资源
解决方法: 共享资源,异步实现对请求的响应和处理
典型代表: 使用工作者线程池的reactor模式设计的web服务器
设计目标: 给定资源阈值尽可能减小响应时间
解决方法: 充分利用资源,独占式处理加快响应
典型代表: hadoop
a) 多进程: 可充分利用多核,资源隔离(一个进程挂掉其他进程不受影响),易于调试,编程简单
b) 多线程: 可充分利用多核,共享资源方便(一个线程挂掉整个程序玩儿完),难以调试,编程复杂
多核机器作为server提供服务的典型模式:
+ 模式1的简单多份拷贝,前提是能使用多个tcp port对外提供服务
+ 主进程 + worker进程,主进程绑定到一个tcp port
必须使用单线程的场景:
I/O密集型任务: 单线程即可,因为增加进程或线程只能加快计算速度,不能加快I/O
计算密集型任务: 推荐多进程,原因如下:
适合多线程的场景需要满足的要求:
使用目的: 用于对响应时间敏感的系统,保证响应时间的前提下使用共享资源的方法尽可能减少对
服务器内存资源的占用.保证响应时间的方式是使得对请求的响应(I/O线程)和对请求的
处理(工作线程)相互重叠,异步处理
以linux服务器集群为例:
8个计算节点,1个控制节点.机器的配置相同.双路四核cpu,千兆以太网互联,编写一个简单的集群
管理软件,由三个程序组成:
1) 运行在控制节点的master,负责监视并控制整个集群的状态
2) 运行在每个计算节点的slave,负责启动和终止job,并监控本机的资源
3) 给用户的client命令行工具,用于提交job
client命令行工具: 交互式程序,提交命令的输入和提交的实际运行异步,使用2个线程
slave: 看门狗进程,负责启动别的job进程,必须是单线程,且其不应该占用太多的cpu资源,适合
单线程
master:
1) 独占8核机器,应当充分利用cpu资源
2) master应当快速响应slave的请求,关注响应时间
3) 集群的状态可完全放入内存中,状态可共享可变
4) master监控的事件有优先级区别
5) master使用多个I/O线程来处理与8个slave之间的TCP连接可降低延迟
6) master需要异步的往本地磁盘写log,logging library有自己的I/O线程
7) master可能要读写数据库,数据库连接这个第三方library可能有自己的线程
8) master可服务于多个client,多个I/O线程可降低用户的响应时间
则master可开启9个线程:
+ 4个与slave通信的I/O线程
+ 2个与client通信的I/O线程
+ 1个logging线程
+ 1个数据库I/O线程
总结:
多线程服务器中的线程一般分为3类:
1) I/O线程: 主循环是I/O Multiplexing,等待在select/poll/epoll系统调用上,也处理定时事件
2) 计算线程: 主循环是阻塞队列,等待在条件变量上,一般位于线程池中
3) 第三方库使用的线程,如logging, DataBase Connection
server一般不会频繁创建和终止线程,一般使用线程池
硬件级别的原子操作
a) 单处理器系统: 能够在单条指令中完成的操作称为原子操作,因为中断只发生在指令边缘
b) 多处理器系统(SMP: Symmetric Multi-Processor): x86平台在指令执行期间对总线加锁
linux内核提供的原子操作接口
软件级别的原子操作的实现依赖于硬件原子操作的支持
a) 对整数操作: atomic_t use_cnt; atomic_set(&use_cnt, 2); atomic_add(3, &use_cnt);等
b) 对位操作: unsigned long word = 0; set_bit(0, &word); clear_bit(5, &word);
change_bit(4, &word);(翻转第4位)等
c++11提供的原子操作接口
a) 通过atomic类模板定义
b) c++11定义了统一的接口,要求编译器产生平台(cpu,如x86_64, ARM)相关的原子操作的具体
实现,接口的成员函数包括 读load(), 写store(), 交换exchange()
为什么要关注原子操作?
a) 软件层面的锁机制也是通过原子操作实现的
b) 原子操作对并发编程很重要,使用互斥锁可以将多部操作变为原子操作,保证这些操作要么
全执行,要么全不执行
c) 简单的场景如计数器可以不用锁,直接使用原子操作
为什么在临界区代码段前后分别加锁和解锁就能保证对共享资源的互斥访问?
附注:
以C语言的编译器为例,完成翻译需要2样东西:
a) C语言到汇编语言的映射规则(编译器实现,相当于把规则落实了)
b) 汇编指令(二进制指令的助记符)到二进制指令的对应规则,cpu平台不同,该规则可能也不同,
因此需要知道当前cpu的指令结构(cpu提供),如在8086 CPU下,jmp对应的指令为11011001
互斥锁解决的问题
互斥锁是为了解决不同线程对同一共享资源的访问冲突问题,至少有一个线程会修改该共享资源,
加上互斥锁后,保证线程对该共享资源的独占,避免其他线程的干扰.只要有写需求都可以使用
互斥锁
为什么不同的线程会访问同一个共享资源?
互斥锁mutex的工作机制
条件变量解决的问题
条件变量的工作机制
int product_count;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
void Producer() {
while (1) {
prepare to increase product_count;
pthread_mutex_lock(&mutex); // 修改product_count前加锁
++product_count;
pthread_mutex_unlock(&mutex); // 修改后解锁
pthread_cond_signal(&cond); // 发出信号
sleep(rand() % 3);
}
}
void Consumer() {
while (1) {
pthread_mutex_lock(&mutex);
// 检测条件
while (product_count < 10)
// 条件不满足,则释放锁,阻塞,为原子操作
// 条件满足,则唤醒,上锁,非原子操作
pthread_cond_wait(&cond, &mutex);
--product_count;
pthread_mutex_unlock(&mutex);
sleep(rand() % 3)
}
}
为什么使用while循环?
为什么pthread_cond_wait在条件不满足时执行原子操作而条件满足时执行非原子操作?
为了允许操作系统的调度,上锁前如果有其他操作,这些操作一定不能是原子的;
为保证条件变量阻塞队列的先进先出,条件不满足时的操作被设计成原子的,即可看为一条指令,
虽然条件满足或不满足都有2步指令,在设计时却将其封装为一条语句主要是方便条件不满足时
的原子操作,但也隐藏了条件满足时的非原子操作,需要程序员自己留意唤醒后上锁前条件可能
被破坏的可能,用while循环来补救
条件变量与信号量的区别
死锁,活锁和饥饿,优先级反转
死锁是两个线程相互占有对方持有的锁,谁也不让谁,彼此等待对方释放锁而僵死
活锁是两个线程都想获得一个锁,同时发出请求,发生碰撞,之后一直尝试请求-碰撞…
饥饿是一个线程始终无法被cpu调度,
如操作系统调度时,优先级低的线程运气一直比较差始终无法获得锁
或条件变量的阻塞队列其中一个线程被唤醒时,老是有新的线程进入,某个运气不好的线程一直
不能被唤醒
优先级反转: 使用锁时出现的调度顺序与优先级不一致的现象: 高优先级任务被低优先级的
任务阻塞,导致高优先级任务迟迟得不到调度,但其他中等优先级的任务却能抢到cpu资源,好像
中优先级任务比高优先级任务有更高的优先权
举例:
三个线程, thread_1(高), thread_2(中), thread_3(低)
t0: thread_3 运行,获得共享资源的锁
t1: thread_2抢占thread_3运行, thread_3睡眠, 但thread_3并未释放锁
t2: thread_1抢占thread_2运行, thread_2睡眠
t3: thread_1需要获得锁,但锁被thread_3持有,且thread_3睡眠,无法释放锁,因此thread_1
睡眠
t4: thread_2和thread_3就绪,因为thread_2优先级更高,因此thread_2被调度运行,thread_1
需要等待thread_2运行完毕且thread_3释放锁后才能运行
分析: thread_1等待低优先级的thread_3释放锁合情合理,但还需要等待thread_2运行完毕就
不合理了,其产生原因是锁等待和操作系统根据优先级的调度之间产生的冲突
解决:
a) 优先级继承,t3时刻thread_1需要获得锁时将thread_3的优先级提升到与thread_1一致
则t4时刻thread_3先被调度执行,thread_3释放锁后,恢复原有的优先级
b) 优先级上限: 给进入临界区的线程都设置为最高优先级,离开后再恢复,直接消除了占有
共享资源时其他进程抢占的可能性
解决死锁:
1) 一次获得所有锁(原子操作)
2) 约定获得锁的顺序
解决活锁: 引入随机性,如sleep(rand()%3)
解决饥饿: 公平锁?
解决2个问题:
文件累积总量过大,无法放在单台服务器上 --> 以文件为单位,分散存储到多台服务器上
存在问题:
数据分布不均衡,文件大小的差距很大,如何才能合理分配到不同服务器上?
若分配时将当前文件大小和所有服务器上的剩余空间大小作比较,选择一个剩余空间最大的服务器,
则空间分配不灵活,且服务器存储空间有浪费:
机器A和B分别剩余200G和100G的空间,先来了一个80G的文件,放到A上,但再来一个150G的,就放不下
了,其实应当把80G放到B上,把150G放到A上,但没人能未卜先知
问题的本质: 文件大小和服务器剩余存储空间的不匹配
解决:
a) 服务器剩余存储空间的不均衡: 操作系统已经解决,读写文件均通过block的方式处理
b) 文件大小的不均衡: 模仿操作系统,以固定大小切分文件,同时维护文件与文件块实际存储位置的
映射的元信息,提供文件读写服务,如HDFS的NameNode和DataNode
分布式存储只有分,没有合
存储的优化,如何才能存的好?
存的好指的是量大又省钱
a) 删数据: 如没有时效性的数据,临时数据/中间结果,同样数据不同业务都有一份
b) 减副本: 临时数据的副本没必要那么多
c) 文件的处理: 压缩,压缩速度和压缩比的权衡,切分,文件格式
d) 分层存储: 不同热度的数据存储在不同介质中,如从热到冷依次存放在内存,SSD硬盘, SATA硬盘
性能指标:
1) 可用性: 机器(单节点)的物理故障无法避免,保障服务不间断只能增加副本
2) 一致性: 主从的数据一致性和时效性
3) 扩展性: 如何实现逻辑分区,逻辑分区和物理节点的映射才能较少增删节点带来的数据移动
如果有查询需求(分布式数据库),如何实现对各种查询模式(范围查询,连表查询等)的快速响应
计算的分治以存储的分治为前提,存储不考虑业务,计算面向业务,需要考虑切分后并行计算结果的合并;
典型的计算遵循map-reduce模式,mapper之间互不干扰,并行处理,对每个block执行map操作,mapper的
个数与block的个数相等,reducer需要对切分后并行计算得到的结果按照业务逻辑做归并,即将属于同一类
的结果归类处理,属于同一类的mapper输出shuffle到一个reducer中处理,reducer的个数即为输出文件的
个数,根据业务逻辑来确定,对mapper的输出划分类别使用partition来完成,partition的个数与reducer的个数
相等
计算的优化,如何才能算得好?
计算与业务相关,需要业务自身考虑优化,通用的计算框架的优化集中于对资源的调度上,即resource
manager,如 YARN, K8S
同样的机器大家共享,采用多租户的方式,有利于减少机器资源的浪费,提升了整体资源利用率,但个体的
独立性受到影响,因此需要合理的资源调度保证个体对资源的需求
解决:
隔离: 计算资源以pool为单位,每个业务可以租用不超过最大配额的计算资源,配额由业务线负责人商定
–> 为避免长期占用资源不归还,设计强杀策略
–> 配额已定,都提交任务,如何为不同的任务分配资源: capacity scheduler, fair scheduler等
调度器
[linux的调度策略]
[YARN的调度策略]
web应用指的是符合B/S架构的应用程序, B: browser, S: server,与传统的 C/S(client/server)架构的区别
是与用户交互的程序特化为浏览器,通过HTTP协议与server通信;
浏览器:
前端负责与用户交互,提供网页,app等可视化界面,接受用户输入并向server发起请求(HTTP或其他应用层
协议),向用户返回从server接收的内容(通过HTTP或其他应用层协议);
前端开发可分为web前端开发(面向浏览器)和客户端开发(面向不同的操作系统,桌面版,移动版)
以web前端为例,需要用到的技术有html, css, javascript及与js相关的技术(如ajax),框架(如react, vue),
js用来处理与用户的交互;
ajax是一种无需加载整个网页的情况下更新部分网页的技术,即只向server请求网页中的一部分数据;
js只是填充了发送请求的内容,实际发送是由浏览器完成的;
前端框架提供了前端业务开发的通用模板,简化程序开发;
web前端技术只是用来实现业务逻辑的,并不关心如何发送HTTP请求(网络IO);
浏览器可认为是提供了html, css, js等运行的环境
vue等框架实现了MVVM(较古老的还有MVC)等设计模式的前端框架,V(View)可理解为html, M(Model)可理解为
从用户接收准备向server提交的数据或从server接收准备填充到view的数据,VM(view model)提供了view向
model和model向view的自动双向数据更新,无需再手工操作control
后端负责接收用户发起的请求,经过业务逻辑处理后(可能需要读写数据库),给用户返回数据
web后端技术并不关系如何接受用户发起的请求和如何返回数据(网络IO),只是用来实现业务逻辑;
接受请求和返回数据由http server实现,比http client复杂得多,需要解决并发问题;
大多数业务逻辑计算任务很少,多为数据的读写,基本都会涉及与数据库的交互,处理的大部分都是文本数据;
为了更清晰的划分职责,方便server端业务逻辑代码的复用,server分为2种: web服务器和应用服务器
web服务器: 只负责接收http请求,返回html格式的响应结果(网络IO),并不关系如何产生响应内容,
若用户请求的是静态页面,直接从服务器的文件目录中取出该页面(文件)返回;
若用户请求的是执行一个动作,则将该请求转发给应用服务器,并向其索要处理结果,然后将处理
结果嵌入到html页面中,发给用户
常见的web服务器: ngix, apache
应用服务器: 只负责接收从web服务器转发过来的动作请求,然后调用运行在其上的业务处理逻辑,获得处理
结果,发送给web服务器;
因此应用服务器一般使用与业务逻辑相同的编程语言实现,使用该编程语言封装与web服务器
交互的数据协议,如HTTP,实现该数据协议的request和response对象,根据请求的动作不同,如
http的get,post,put,delete,分发到不同的handle中处理,在handle中取参数实现业务逻辑,如
对数据库的增删改查,并通过数据协议发送出去,因此应用服务器一般都实现了web服务器的功能
常见的应用服务器: tomcat, jetty
为什么需要区分web服务器和应用服务器?
为了解耦,使得业务逻辑代码可以在多端复用,解除与html的强绑定;
应用服务器相当于提供了方法调用,其业务处理逻辑可被不同的调用者请求,可以是使用HTTP协议的web服务器
,也可以是使用其他协议的调用者,如来自andrioid客户端的调用,来自windows客户端的调用
前后端分离
前端代码(包括html,css,js)存储在web服务器中,运行在用户的浏览器中,需要用户发起一个http请求从web
服务器获取;
前端追求: 页面表现,速度流畅,兼容性,用户体验
后端代码存储且运行在应用服务器中,负责实现业务逻辑;
后端追求: 三高(高并发,高可用,高性能),安全,存储
前后端耦合
前后端耦合指的是前端代码与后端代码混合在一起,目的是通过java代码运行后产生视图发送给用户,类似js的
功能,负责与用户的交互,只是运行在server端,可提供较复杂的交互,如查询数据库,在与浏览器之间传输时jsp
源码是不可见的,而js代码是浏览器直接download下来的,是可见的.jsp不使用web服务器,只有应用服务器,
==前后端耦合的例子==:
java后端分为三层: 控制层(controller), 业务层(service), 持久层(dao)
控制层: 负责接受参数,调用业务层,封装数据,路由,渲染到jsp页面
jsp页面使用各种标签(jstl/el/struts等)或手写java表达式将后台的数据展现出来(视图层)
存在问题:
前后端耦合时开发流程:
前后端耦合的请求方式:
前后端分离:
将前端代码和后端代码完全分离,通过约定好的的restful接口通信(web服务器转发用户的请求调用应用服务
器中的业务逻辑),数据格式一般采用json,调用方式一般采用ajax
前后端分离的开发流程:
前后端分离的请求方式:
前后端分离对并发的支持:
大量并发浏览器请求 --> web服务器集群 --> 应用服务器集群 --> 文件/数据库/缓存/消息队列服务器集群
restful api是什么?
restful api定义了前后端交互(web服务器与应用服务器)的接口形式(不是客户端与前端/web服务器交互的接口
url),通过http的方式请求,使用动词+名词的组合,动词表示动作,名词表示资源的表现形式(包括路径),类似RPC?
(thrift等)
REST: representation state transfer
资源: 网络上的一个具体信息,与uri一一对应
uri: uniform resource identifier,能唯一标识一个资源,详细的路径信息
url: uniform resource location,统一资源定位符,可能只包含名字,无法获知路径
represetation: 资源的表现层,即资源的表现形式,图片可以有jpg,也可以有png格式
state transfer: 状态转移,资源的状态发生变化,作用于资源的具体表现形式
restful 规定的语义: POST: 增 DELETE: 删 PUT: 改 GET: 查
为什么像mysql, redis都提供了自己的server?
因为他们是数据库,完全独立于业务逻辑,必须为业务逻辑提供一种调用方式来完成对数据库的操作;
业务逻辑代码使用数据库自己的数据操作规则,如SQL语句,需要将其放在数据库中执行,因此必须有种通信机制使得
在业务逻辑中创建的sql语句能够传输到数据库中执行,数据库server提供了这种机制,本质上server都只处理网络
IO,封装请求和响应,调度处理函数,不同的server使用的应用层协议会有所不同,根据实际需求来定;
web服务器是应用服务器的client, 应用服务器是数据库服务器的client
使用者的编程语言与其使用的server之间的关系?
诸如mysql等数据库的server,client向其发出请求,server内部完全自己处理请求,获得结果后封装成响应
发送给client,server的处理使用自己的编程语言,与client的编程语言完全解耦,只需要提供结果即可
形如应用服务器这样的server除了能够接受请求,返回响应之外,其handle函数需要实现业务逻辑,需要程序员
自己去实现该业务逻辑,本质上是一套框架,与业务逻辑共用一套编程语言
形如ngix这样的web服务器由于其只负责静态页面的返回,来了一个请求,要么直接去文件目录下取html返回,
要么转发该请求给应用服务器,收到应用服务器处理后的结果后再装填进html或者直接返回给用户(如ajax),
并不涉及handle函数,只有网络IO,因此可当做一个独立的软件来使用,只要在规定的目录下放上html,css,js
文件即可
综上,如果一个server可以设计成与其使用者的开发语言无关的,必须是如下2种情况的一种:
如果server与使用者的开发语言无关,使用者需要配置server,改改参数啥的,美其名曰优化;
如果server与使用者的开发语言信管,使用者需要实现server的handle函数,完成具体的业务逻辑,美其名曰部署
client的编程语言和server的编程语言之间的关系?
完全可以不同,因为client和server规定了通信的协议,如HTTP, thrift, protobuffer,
通信协议本质上是约定好数据格式以及对数据进行的操作,双方约定好能彼此解析即可,
通信协议与语言无关,client和server可使用各自的语言
常用server的网络IO模型:
reactor模式: 基于IO复用的单线程事件轮询实现读写 + 工作者线程池,详见 2.1节小王开店的例子
为什么很少看到使用c++实现的应用服务器,即使用c++来开发web后端?
+ 只支持ascii码字符,不支持任何其他编码方式,如unicode, utf-8,中文怎么表示?
+ std::string只是对字符数组的封装,字符的本质还是字节,还能使用下标访问,越界访问怎么办
+ 想做一下字符串的切分和拼接都很费劲,没有内置函数
并发数: 服务器的可用资源 / 单个请求耗费的资源
提高并发数需要开源节流,
开源: 增加服务器的可用资源 or 更高效的利用服务器的资源
节流: 减少单个请求耗费的资源
优化方法一: 增加服务器的可用资源
a) 更快的cpu,更多核心的cpu,更大的内存,更快的磁盘,更宽的网络带宽(经费在燃烧)
b) 分担流量压力,增加服务器个数,美其名曰水平扩展,或负载均衡
优化方式二: 更高效的利用服务器资源
a) 水平扩展,使用多个服务器,每个服务器处理一类业务,专司其职
b) 数据库分库分表,每个库或每个表专司其职
c) 增加缓存,存放热点数据
d) 使用多进程,多线程或协程
e) 使用消息队列异步更新用户不关心的系统
f) 数据库读写分离
g) 优化代码逻辑,优化代码语句
优化方式三: 减少单个请求耗费的资源
a) ajax提交,只请求需要更新的数据
b) 使用浏览器缓存机制
c) 压缩文件
d) 使用CDN