从并发到分布式系统和web应用

文章目录

    • [本人github上tcp reactor server的实现](https://github.com/Baoshan-Hobbit/reactor_server)
  • 1. 并发
    • 1.1 并发与并行
    • 1.2 软件系统运行的指标
    • 1.3 实现并发的技术
    • 1.4 多线程同步
      • 1.4.1 原子操作: 不可中断的一个或一系列操作
      • 1.4.2 互斥锁与条件变量
  • 2. 分布式系统
    • 2.1 为什么需要分布式系统?
    • 2.2 分布式存储引擎
    • 2.3 分布式计算框架
  • 3. web服务器
    • 3.1 什么是web应用?
    • 3.2 C/S与B/S的区别?
    • 3.3 前端与后端到底是什么?
    • 3.4 对server的理解
    • 3.5 影响web应用并发数的因素有哪些?如何优化?

本人github上tcp reactor server的实现

1. 并发

1.1 并发与并行

  1. 并发指的是程序在一段时间内可服务多个用户,可通过多进程或多线程实现.
  2. 并行是对计算需求提出的,指的是同一时刻可同时处理的任务数,与cpu的核心数相等

1.2 软件系统运行的指标

  1. 吞吐量(批处理,高吞吐,高延迟): 单位时间内处理的请求数,吞吐量时系统的综合指标,硬件
    层面的cpu/磁盘/网络的任何一种都有可能称为瓶颈,软件层面的数据库,代码逻辑也对吞吐量
    造成影响

    I/O密集型应用: 非常消耗I/O资源,很少使用cpu资源,典型代表是web应用
    计算密集型应用: 非常消耗cpu资源,很少使用I/O资源,典型代表是机器学习算法的训练

  2. 响应时间(实时处理,低吞吐,低延迟): 单个请求从发出请求到收到响应消耗的时间,一般使用
    平均响应时间(所有用户的总响应时间 / 用户数)

  3. 并发数: 系统能同时承受的最大用户数

  4. QPS: querys per second,反映了服务器接受请求的能力,有可能一个网页的请求发送了多个
    query

  5. TPS: trasactions per second,反映了服务器处理完整请求的能力,如一个完整网页(包括多个
    请求)从请求到返回算做一个事务

以饭店为例,小王从烹饪学校学成归来,自己开了一家小饭店,

  1. 由于资金有限,一开始只有他一个人单干,招呼,点菜,做菜,端菜,结账统统一个人来,为了避免
    顾客等待,他一次只服务一个顾客,当前一个顾客酒足饭饱后才开始接待第二个顾客,因此大家
    觉得去他家吃一顿饭等待的时间太长了(等待时老板还不搭理你),因此客流量惨淡.小王反思后
    决定提高自己的业务水平,很快他干活儿麻溜了许多,顾客的响应时间大幅降低,小店的吞吐量
    也上去了,并发数增加. --> 单线程reactor server

  2. 一个月后,小王干活的速度再也提不上去了,他想了想,给一个顾客做饭时却耽误了接待下一个
    顾客,白白丢了生意,因此他拉来自己的妻子帮他当服务员,负责招呼,点菜,端菜,结账等,而他
    则专职做菜.这样对顾客请求的响应和处理分隔开,可以同时进行(处理前一个顾客的请求时
    不影响招呼下一个顾客),这样虽然顾客从进店到吃完离开总的时间虽然没变,但是进店就能响应
    自己对点菜的需求(虽然还是得等),顾客的体验变好(响应了一部分请求),因此店里的客流量
    开始增加(老板的厨艺还是不错的,值得等待).顾客感受到的响应时间(只是部分请求的响应,
    实际总的响应时间并无改变)变短,店里吞吐量不变,并发数增加

  3. 小王的夫妻店生意越来越好,但是顾客的最长响应时间(最后一个光临的顾客)也随之增加(假设
    妻子点菜等不花时间,最长响应时间=前面等待顾客数 * 小王做菜的时间),因为小王的做菜速度
    已经达到了极限.小王于是招了一个厨师,做菜的速度快了一倍,响应时间减半,吞吐量翻番,并发
    数翻番 --> 工作者线程池reactor server

  4. 顾客越来越多,妻子甚至也忙不过来了,因此小王决定让妻子专门负责在前台招呼顾客,负责分配
    座位,结账,另外招聘了3个服务员负责点菜,端菜,顾客的总响应时间和店里的吞吐量并无变化,
    但是顾客感受到的响应时间减少,并发数增加 --> 多reactor server

总结:

  1. 吞吐量只与工作者的处理能力相关,这里的处理能力可以是cpu(计算密集型)也可以是磁盘/网络
    I/O, 对计算密集型任务增加线程数可近似成倍增加处理能力,对I/O密集型任务增加线程无济于
    事,因为线程和进程都是相对于cpu而言的,他们占用的是cpu时间
  2. 总响应时间(一个用户完整的访问请求,即事务)与吞吐量成反比,分离对用户请求的接受/响应和
    对请求的处理可减少用户感知的响应时间,提高系统的并发数
  3. 若要提高并发数的同时不至于使用户的请求等待处理的时间过长根本上还是得提高工作者的
    处理能力,对于I/O密集型应用,可考虑加入缓存减小I/O的响应时间

常见软件系统的分类:

  1. 对响应时间敏感的系统,如web应用,在线交易系统
 设计目标: 给定响应时间阈值尽可能少的使用系统资源                         
 解决方法: 共享资源,异步实现对请求的响应和处理                            
 典型代表: 使用工作者线程池的reactor模式设计的web服务器                   
  1. 对吞吐量敏感的系统,如批处理系统
 设计目标: 给定资源阈值尽可能减小响应时间                                 
 解决方法: 充分利用资源,独占式处理加快响应                                
 典型代表: hadoop

1.3 实现并发的技术

a) 多进程: 可充分利用多核,资源隔离(一个进程挂掉其他进程不受影响),易于调试,编程简单
b) 多线程: 可充分利用多核,共享资源方便(一个线程挂掉整个程序玩儿完),难以调试,编程复杂

多核机器作为server提供服务的典型模式:

  1. 只有一个单线程的进程: 不可伸缩,不能发挥多核机器的计算能力
  2. 只有一个多线程的进程
 + 模式1的简单多份拷贝,前提是能使用多个tcp port对外提供服务                
 + 主进程 + worker进程,主进程绑定到一个tcp port                            
  1. 含有多个单线程的进程
  2. 含有多个多线程的进程

必须使用单线程的场景:

  1. 程序可能会调用fork(), [待学习]
  2. 限制程序的cpu使用率,如监控其他进程的状态的进程,避免过分的抢夺系统的计算资源.
    如在一个8核的机器上,单线程程序最高cpu使用率也只有12.5%,只占一个核

I/O密集型任务: 单线程即可,因为增加进程或线程只能加快计算速度,不能加快I/O

计算密集型任务: 推荐多进程,原因如下:

  1. 多进程在多核上可实现并行
  2. 多进程资源隔离,只需要对数据切分然后分别独立处理即可(map, reduce),不需要太多的
    数据共享
  3. 多进程编程简单,调试简单
  4. 多线程共享资源,还需要额外增加同步机制
  5. 多线程编程复杂,调试复杂
  6. 当然也可以使用多个进程,在单个进程内使用多线程

适合多线程的场景需要满足的要求:

使用目的: 用于对响应时间敏感的系统,保证响应时间的前提下使用共享资源的方法尽可能减少对
服务器内存资源的占用.保证响应时间的方式是使得对请求的响应(I/O线程)和对请求的
处理(工作线程)相互重叠,异步处理

  1. 多核cpu机器
  2. 应用关注响应时间
  3. 线程需要共享数据且需要修改
  4. 事件有优先级差异,可使用专门的线程处理高优先级时间 [待学习]
  5. 应用需要异步操作,如logging
  6. 程序可伸缩,应当能够享受增加cpu数目带来的好处
  7. 具有可预测的性能,随着负载增加,性能缓慢下降,超过某个临界点后急速下降,线程数据不随
    负载变化
  8. 多线程能清晰的划分功能,使得每个线程的逻辑比较简单,任务单一,便于编程

以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一般不会频繁创建和终止线程,一般使用线程池

1.4 多线程同步

1.4.1 原子操作: 不可中断的一个或一系列操作

  1. 硬件级别的原子操作
    a) 单处理器系统: 能够在单条指令中完成的操作称为原子操作,因为中断只发生在指令边缘
    b) 多处理器系统(SMP: Symmetric Multi-Processor): x86平台在指令执行期间对总线加锁

  2. 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位)等

  3. c++11提供的原子操作接口
    a) 通过atomic类模板定义
    b) c++11定义了统一的接口,要求编译器产生平台(cpu,如x86_64, ARM)相关的原子操作的具体
    实现,接口的成员函数包括 读load(), 写store(), 交换exchange()

  4. 为什么要关注原子操作?
    a) 软件层面的锁机制也是通过原子操作实现的
    b) 原子操作对并发编程很重要,使用互斥锁可以将多部操作变为原子操作,保证这些操作要么
    全执行,要么全不执行
    c) 简单的场景如计数器可以不用锁,直接使用原子操作

为什么在临界区代码段前后分别加锁和解锁就能保证对共享资源的互斥访问?

  1. 临界区代码访问了共享资源
  2. 加锁的本质是对一个大家约定好的全局变量赋值,通过值的状态来决定当前进程的行为,以
    互斥锁为例,如果该全局变量值为1,则当前进程进入睡眠,不再往下执行代码,否则,当前进程
    修改该值为1,执行临界区的代码,其他进程看到值为1后就遵守约定进入睡眠,当前进程执行
    完毕后修改该全局变量的值为0,其他进程就可以对该全局变量做修改,即上锁.这样实现了对
    共享资源的互斥访问.加锁和释放锁本质上都是对全局变量的修改,需要使用原子操作保证该
    修改在一条指令中完成

附注:

  1. 编程语言: 本质上是一套规则的集合,方便程序员编写这些规则组成的文本,即程序代码
  2. 编译器: 本质上是一个翻译工具,将程序代码翻译为cpu可以理解的二进制代码;
  以C语言的编译器为例,完成翻译需要2样东西:                                 
   a) C语言到汇编语言的映射规则(编译器实现,相当于把规则落实了)              
   b) 汇编指令(二进制指令的助记符)到二进制指令的对应规则,cpu平台不同,该规则可能也不同,
      因此需要知道当前cpu的指令结构(cpu提供),如在8086 CPU下,jmp对应的指令为11011001
  1. 操作系统: 本质上也是在cpu上运行的应用程序,只是其功能特化为对硬件资源的管理和对其他
    应用程序的调度,因此由编程语言编写,由于编译器也是一种应用程序,因此也需要操作系统的
    管理和调度,在操作系统提供的环境中执行,接受操作系统的领导
  2. 为什么汇编语言比C语言更快?
    理论上是一样快的,只是由于采用了编译器的自动翻译,原本只需要2句指令就能完成的任务现在
    可能需要10句,指令增多了,cpu执行的时间也更长了

1.4.2 互斥锁与条件变量

互斥锁解决的问题
互斥锁是为了解决不同线程对同一共享资源的访问冲突问题,至少有一个线程会修改该共享资源,
加上互斥锁后,保证线程对该共享资源的独占,避免其他线程的干扰.只要有写需求都可以使用
互斥锁

为什么不同的线程会访问同一个共享资源?

  1. 在实际编程中为了充分利用多核优势,加快程序执行速度,多个线程执行相同的代码段,
    该代码段包含对共享资源的访问
  2. 多个线程各自使用自己的代码段,但这些代码段中都包含对同一个共享资源的额访问

互斥锁mutex的工作机制

  1. 对互斥量加锁后,任何其他试图对互斥量再次加锁的线程将会阻塞(睡眠)直到当前线程释放该
    互斥锁;
  2. 如果释放互斥锁时有多个线程阻塞,所有阻塞线程都会变为就绪状态,由cpu的调度算法决定
    哪一个线程可以获得锁,其他线程仍然阻塞

条件变量解决的问题

  1. 条件变量设计到至少两种角色: 生产者(一定是写)和消费者(不一定写,也可能只读),两者
    通过一个全局变量通信,且消费者只有全局变量满足一定条件时才开始消费
  2. 要保证生产和消费的互斥,使用互斥锁完全可满足要求,生产时加锁,消费时也加锁,但在生产
    的前期阶段,条件未满足时,消费者仍然需要频繁的加锁解锁,造成cpu资源的浪费
  3. 条件变量可使得定制的条件不满足时,线程阻塞在该条件变量上,避免了cpu资源的浪费

条件变量的工作机制

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循环?

  1. 使用while循环不是占用cpu忙等,因为pthread_cond_wait本身就是系统调用,当条件不满足时
    阻塞(睡眠),不需要程序自己用while实现等待的效果;
  2. 当条件满足时pthread_cond_wait包含2条操作:
    a) 从条件变量的阻塞队列唤醒当前线程
    b) 对共享资源上锁
    这2条操作并非是原子操作,因此当唤醒后上锁前其他线程可能已经修改了共享资源(迅速完成了
    上锁,消费,释放锁的操作),导致条件不再满足,但当前线程对此不知情,依然正常完成了上锁,
    准备消费,然而此时条件已经改变,因此只能重新检测条件是否满足,避免在条件改变时错误
    的进行消费

为什么pthread_cond_wait在条件不满足时执行原子操作而条件满足时执行非原子操作?

  1. 条件不满足时的阻塞应当时把当前线程放到cond的阻塞队列中,要保证先进先出的顺序,
    不能让两个线程乱序插入阻塞队列,即要保证线程A先发现条件不满足,则必须先进入阻塞队列
    反例: 线程A先检测到条件不满足,反而后进入条件变量的阻塞队列
    t0, t1, t2, t3 t4 t5
    A lock cond wrong,unlock sleep
    B lock cond wrong,unlock sleep
  2. 条件满足时阻塞队列中的所有线程均被唤醒,而此时共享资源未被锁定,所有线程均接受操作
    系统的调度准备上锁,如果唤醒和上锁为原子操作,若唤醒是同时发生的,则谁也别想得到锁,
    若唤醒并非严格同时,则最快被唤醒的必然得到锁,排除了操作系统调度的可能性

为了允许操作系统的调度,上锁前如果有其他操作,这些操作一定不能是原子的;
为保证条件变量阻塞队列的先进先出,条件不满足时的操作被设计成原子的,即可看为一条指令,
虽然条件满足或不满足都有2步指令,在设计时却将其封装为一条语句主要是方便条件不满足时
的原子操作,但也隐藏了条件满足时的非原子操作,需要程序员自己留意唤醒后上锁前条件可能
被破坏的可能,用while循环来补救

条件变量与信号量的区别

  1. 信号量与条件变量的功能相同,都是为了满足"条件满足时再唤醒"的需求
  2. 信号量只能使用"计数条件",且条件满足只是意味着计数值>0
  3. 条件变量可以使用各种自定义条件,更加灵活,事实上可使用条件变量实现信号量

死锁,活锁和饥饿,优先级反转

  1. 死锁是两个线程相互占有对方持有的锁,谁也不让谁,彼此等待对方释放锁而僵死

  2. 活锁是两个线程都想获得一个锁,同时发出请求,发生碰撞,之后一直尝试请求-碰撞…

  3. 饥饿是一个线程始终无法被cpu调度,
    如操作系统调度时,优先级低的线程运气一直比较差始终无法获得锁
    或条件变量的阻塞队列其中一个线程被唤醒时,老是有新的线程进入,某个运气不好的线程一直
    不能被唤醒

  4. 优先级反转: 使用锁时出现的调度顺序与优先级不一致的现象: 高优先级任务被低优先级的
    任务阻塞,导致高优先级任务迟迟得不到调度,但其他中等优先级的任务却能抢到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. 分布式系统

2.1 为什么需要分布式系统?

解决2个问题:

  1. 单台机器算的慢,哪怕多进程,多线程,协程全用上 --> 分布式计算框架
  2. 单台机器存不下 --> 分布式存储引擎(引擎实际上也是框架,提供解决特定业务问题的通用模板)
    本质是分治方法的应用,先做切分,然后再汇总

2.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) 扩展性: 如何实现逻辑分区,逻辑分区和物理节点的映射才能较少增删节点带来的数据移动
如果有查询需求(分布式数据库),如何实现对各种查询模式(范围查询,连表查询等)的快速响应

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的调度策略]

3. web服务器

3.1 什么是web应用?

web应用指的是符合B/S架构的应用程序, B: browser, S: server,与传统的 C/S(client/server)架构的区别
是与用户交互的程序特化为浏览器,通过HTTP协议与server通信;

浏览器:

  1. 实现HTTP协议的客户端部分(基于TCP socket?),用于向server发送请求,获取返回的HTTP响应
  2. 解析器,解析从server收到的HTML页面,渲染后展示给用户

3.2 C/S与B/S的区别?

  1. B/S架构其实是C/S架构的一种,只是B/S可以实现平台无关,无论是windows, linux, android, apple只要有
    浏览器即可被用来运行web应用程序
  2. 而C/S架构一般是平台相关的,对每个平台都需要至少重新开发一遍前端;
  3. C/S架构更加灵活,如可以根据业务需求实现自己的应用层协议,不一定限制于HTTP协议,如QQ, 网易云,
    爱奇艺等,android, apple等移动应用(android开发其实属于前端开发),windows, ubuntu等桌面版应用
    都属于C/S架构

3.3 前端与后端到底是什么?

  1. 前端负责与用户交互,提供网页,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

  1. 后端负责接收用户发起的请求,经过业务逻辑处理后(可能需要读写数据库),给用户返回数据

    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客户端的调用

  2. 前后端分离
    前端代码(包括html,css,js)存储在web服务器中,运行在用户的浏览器中,需要用户发起一个http请求从web
    服务器获取;
    前端追求: 页面表现,速度流畅,兼容性,用户体验
    后端代码存储且运行在应用服务器中,负责实现业务逻辑;
    后端追求: 三高(高并发,高可用,高性能),安全,存储

    前后端耦合
    前后端耦合指的是前端代码与后端代码混合在一起,目的是通过java代码运行后产生视图发送给用户,类似js的
    功能,负责与用户的交互,只是运行在server端,可提供较复杂的交互,如查询数据库,在与浏览器之间传输时jsp
    源码是不可见的,而js代码是浏览器直接download下来的,是可见的.jsp不使用web服务器,只有应用服务器,

    1. 将java代码填充进html中运行后发送给用户,典型的jsp页面,jsp是servlet的一种,运行时需要首先转化为
      servlet, servlet只是定义了一个接口,该接口能解析html;
    2. 把html嵌入进java代码中,运行后发送给用户,典型的servlet模式;
==前后端耦合的例子==:                                                         
 java后端分为三层: 控制层(controller), 业务层(service), 持久层(dao)        
 控制层: 负责接受参数,调用业务层,封装数据,路由,渲染到jsp页面               
 jsp页面使用各种标签(jstl/el/struts等)或手写java表达式将后台的数据展现出来(视图层)

存在问题:

  1. 没必要在服务器端关心视图(用户看到什么页面),视图的渲染应当利用用户的资源
  2. 无web服务器,动态资源和静态资源耦合,并发量增大时,对应用服务器的资源消耗比较大,应用服务器的i/o
    很容易成为瓶颈
  3. 第一次请求jsp,必须在应用服务器中编译成servlet,响应慢
  4. 每次请求jsp都是访问servlet再用输出流输出的html页面,效率比直接使用html低
  5. jsp中有众多的标签,表达式,前端工程师修改页面时费劲
  6. jsp中动态内容很多时,加载慢
  7. 前端工程师需要配置java的开发环境

前后端耦合时开发流程:

  1. 产品经理,领导,客户提需求
  2. UI做设计图
  3. 前端工程师做出html页面
  4. 后端工程师将html页面套成jsp页面
  5. 集成出现问题
  6. 前端返工
  7. 后端返工
  8. 二次集成
  9. 集成成功
  10. 交付

前后端耦合的请求方式:

  1. 在已接收的jsp页面中客户端请求
  2. server的servlet或controller接收请求
  3. 调用service, dao代码完成业务逻辑
  4. 返回jsp
  5. jsp在客户端展现动态的效果
    后端实现mvc,c(控制路由),m(业务逻辑),v(渲染视图),后端任务重

前后端分离:
将前端代码和后端代码完全分离,通过约定好的的restful接口通信(web服务器转发用户的请求调用应用服务
器中的业务逻辑),数据格式一般采用json,调用方式一般采用ajax

前后端分离的开发流程:

  1. 产品经理,领导,客户提需求
  2. UI做设计图
  3. 前后端约定接口,参数
  4. 前后端并行开发(即使需求变了,只要接口参数不变,不用两边都改代码)
  5. 前后端集成
  6. 前端页面调整
  7. 集成成功
  8. 交付

前后端分离的请求方式:

  1. 在已接收的html页面中客户端请求
  2. web服务器接收请求
  3. web服务器转发请求到应用服务器
  4. 应用服务器调用业务逻辑,返回json结果给web服务器
  5. web服务器将结果填充进html中发送给客户端
    前端(web服务器)实现control, view,后端(应用服务器)实现model(业务逻辑),前端任务重
    现在前端框架中的mvc, mvvm等模式中的m并不严格,其实是指应用服务器返回的结果,而非业务逻辑

前后端分离对并发的支持:
大量并发浏览器请求 --> 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: 查

3.4 对server的理解

为什么像mysql, redis都提供了自己的server?

因为他们是数据库,完全独立于业务逻辑,必须为业务逻辑提供一种调用方式来完成对数据库的操作;
业务逻辑代码使用数据库自己的数据操作规则,如SQL语句,需要将其放在数据库中执行,因此必须有种通信机制使得
在业务逻辑中创建的sql语句能够传输到数据库中执行,数据库server提供了这种机制,本质上server都只处理网络
IO,封装请求和响应,调度处理函数,不同的server使用的应用层协议会有所不同,根据实际需求来定;
web服务器是应用服务器的client, 应用服务器是数据库服务器的client

使用者的编程语言与其使用的server之间的关系?

  1. 诸如mysql等数据库的server,client向其发出请求,server内部完全自己处理请求,获得结果后封装成响应
    发送给client,server的处理使用自己的编程语言,与client的编程语言完全解耦,只需要提供结果即可

  2. 形如应用服务器这样的server除了能够接受请求,返回响应之外,其handle函数需要实现业务逻辑,需要程序员
    自己去实现该业务逻辑,本质上是一套框架,与业务逻辑共用一套编程语言

  3. 形如ngix这样的web服务器由于其只负责静态页面的返回,来了一个请求,要么直接去文件目录下取html返回,
    要么转发该请求给应用服务器,收到应用服务器处理后的结果后再装填进html或者直接返回给用户(如ajax),
    并不涉及handle函数,只有网络IO,因此可当做一个独立的软件来使用,只要在规定的目录下放上html,css,js
    文件即可

    综上,如果一个server可以设计成与其使用者的开发语言无关的,必须是如下2种情况的一种:

    • server只负责网络IO,并无handle这样的计算过程
    • server有handle函数,但是其使用者不需要知道具体的实现,只关心取到结果

    如果server与使用者的开发语言无关,使用者需要配置server,改改参数啥的,美其名曰优化;
    如果server与使用者的开发语言信管,使用者需要实现server的handle函数,完成具体的业务逻辑,美其名曰部署

client的编程语言和server的编程语言之间的关系?
完全可以不同,因为client和server规定了通信的协议,如HTTP, thrift, protobuffer,
通信协议本质上是约定好数据格式以及对数据进行的操作,双方约定好能彼此解析即可,
通信协议与语言无关,client和server可使用各自的语言

常用server的网络IO模型:
reactor模式: 基于IO复用的单线程事件轮询实现读写 + 工作者线程池,详见 2.1节小王开店的例子

为什么很少看到使用c++实现的应用服务器,即使用c++来开发web后端?

  1. web应用在client和server间传输的基本都是文本(字符串)
  2. c++对字符串的支持极其垃圾
+ 只支持ascii码字符,不支持任何其他编码方式,如unicode, utf-8,中文怎么表示?   
+ std::string只是对字符数组的封装,字符的本质还是字节,还能使用下标访问,越界访问怎么办
+ 想做一下字符串的切分和拼接都很费劲,没有内置函数                           

3.5 影响web应用并发数的因素有哪些?如何优化?

并发数: 服务器的可用资源 / 单个请求耗费的资源
提高并发数需要开源节流,
开源: 增加服务器的可用资源 or 更高效的利用服务器的资源
节流: 减少单个请求耗费的资源

  1. 服务器的可用资源:
  • cpu并发处理能力: 假设16核cpu,以多线程的方式执行业务逻辑,每个请求耗费的cpu时间为20ms,则1s内可接受
    的并发数为1000/20*16=800
    提高并发数可以
    a) 换用更多核心数的cpu
    b) 换用单核运算速度更快的cpu
  • 内存: 假设总的内存大小为8G,每个请求耗费的内存空间为20M,则可同时应对的并发数为 8*1024/20=408
  • 网络带宽: 假设上下行总带宽为100M,单个请求耗费的带宽为1M,则可同时应对的并发数为 100/1=100
  • 磁盘IO速度: 假设单个请求需要读取10M文件,磁盘的IO速度为100M/s,则1s内可应对的并发数为 100/10=10
优化方法一: 增加服务器的可用资源                                            
  a) 更快的cpu,更多核心的cpu,更大的内存,更快的磁盘,更宽的网络带宽(经费在燃烧)
  b) 分担流量压力,增加服务器个数,美其名曰水平扩展,或负载均衡                
  1. 对服务器资源的利用方式:
  • 不同的业务类别对服务器的要求不同,如文件下载,图片下载需要定制更高效的压缩方式,不同的服务存放在
    同一台服务器上,互相耦合,服务器众口难调,配置优化困难
  • 热点数据放在数据库中是对磁盘操作,速度比较慢
  • 服务器的多核处理能力需要得到充分的利用
  • 一个请求需要与多个系统交互,有些交互用户不需要关心,可异步执行
  • 数据库的读写彼此耦合
  • 代码中有耗时的或耗费内存的逻辑或语句,可以优化
优化方式二: 更高效的利用服务器资源                                          
  a) 水平扩展,使用多个服务器,每个服务器处理一类业务,专司其职                
  b) 数据库分库分表,每个库或每个表专司其职                                  
  c) 增加缓存,存放热点数据                                                  
  d) 使用多进程,多线程或协程                                                
  e) 使用消息队列异步更新用户不关心的系统                                   
  f) 数据库读写分离                                                         
  g) 优化代码逻辑,优化代码语句                                              
  1. 单个请求耗费的资源
  • 每次都请求完整的html页面,有些元素不发生变化,没必要更新
  • http请求每次都是发起请求,等待结果,收到响应三步,如果有些数据可以直接使用上次收到的数据,没必要
    更新或者有必要更新但是发起请求后发现和后端的结果和上次一致,那么后端也不用再响应
  • 图片,文件等体积较大,可考虑压缩传输,节省带宽
  • 将服务器放在离用户更近的地方,节省传输时间
优化方式三: 减少单个请求耗费的资源                                                                                                                                                                      
  a) ajax提交,只请求需要更新的数据                                          
  b) 使用浏览器缓存机制                                                     
  c) 压缩文件                                                               
  d) 使用CDN

你可能感兴趣的:(并发编程,web开发,分布式)