從併發到分佈式系統和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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章