《億級流量網站架構核心技術》讀書筆記

豆瓣連接html

實驗java

  1. nginx的tcp負載均衡
  2. consul+consul-template
  3. consul實現配置中心
  • 一個系統不是一會兒就能設計完美的
  • 在有限的資源下,優先解決最核心問題

1、原則

  1. 高併發
    1. 無狀態
    2. 拆分
    3. 服務化
    4. 消息隊列(異步,)
      1. 大流量緩存(先入redis,再同步到db)
      2. 數據校對
    5. 數據異構(相似數據冗餘)
      1. 數據異構(相似數據冗餘,來提高讀取效率,例如分表)
      2. 數據閉環,對於須要屢次查詢數據的接口,能夠把全部數據緩存一次,提高讀的速度,而後各個數據修改後,都來改這個緩存
    6. 緩存,
      1. 瀏覽器緩存
      2. app客戶端緩存
      3. cdn緩存
      4. 接入層緩存(Nginx,緩存整個接口)
      5. 應用層緩存
        1. java的線程共享
        2. redis緩存
      6. 分佈式緩存
        1. redis緩存
        2. 緩存有多個層級,例如
          1. 接入層(Nginx)
          2. Redis
          3. java的緩存(Python沒有)
          4. 回源(數據庫,或者調API)
    7. 併發化(若是須要多個IO,可使用併發獲取,而不是串行)
  2. 高可用
    1. 降級(提供有損服務)
      1. 集中開關管理(也就是一個統一的配置後臺,設置降級後,全部服務都能識別)
      2. 可降級的多級讀服務(對於上面的緩存層級,能夠設置最終會去到哪一層。例如Redis壓力很大,就能夠設置接入層獲取不到緩存,就直接返回了,而不繼續往下找)
        1. 這這裏,返回什麼也是個問題,能夠返回空列表(會不會被吐槽?),若是是狀態,能夠返回個默認值,例若有貨
      3. 業務降級
        1. 屏蔽次要功能,例如雙十一,淘寶查歷史帳單,只會查近1個月的
        2. 次要流程改成異步,例如微信搶紅包,搶到紅包是重要功能,紅包金額的入帳就是次要功能
    2. 限流(限制惡意流量,防止流量超過峯值)
      1. 主動拒絕,返回友好點的文案
    3. 切流量(機器掛了,切流量到其餘正常的機器)
    4. 可回滾(代碼版本可回滾)

業務設計原則node

  1. 冪等
  2. 流程可定義(也就是有流水錶)
  3. 狀態修改(使用CAS)
  4. 管理後臺審計
  5. 文檔和註釋
  6. 備份(代碼和人員)

2、負載均衡和反向代理

接入層、反向代理、負載均衡,通常都是指Nginxpython

  • 負載均衡(Nginx的upstream)
    • 算法
      • 輪詢(weight來指定權重)
      • ip hash 同一個ip去同一臺機
      • 其餘hash
    • 失敗重試
      • 在fail_timeout時間內若是失敗max_fails次,認爲不可用,在fail_timeout後,從新檢測
    • 健康檢查
      • 默認是惰性的(應該是請求來纔去檢測的意思)
      • nginx_upstream_check_module(插件,每n秒請求上游服務,返回2xx或者3xx,表示存活,因此上游服務要作好對接)
  • 反向代理
    • 緩存(存放在tmpfs)

動態負載均衡

若是修改upstream,比較麻煩,須要重啓nginx。動態負載均衡就是能夠自動發現上游服務器,而後經過管理後臺,快速註冊或者摘取上游服務器mysql

方案一:consul+consul-template

流程nginx

  • 上游服務向consul_server註冊節點
  • 管理後臺向consul_server註冊或者摘除節點
  • consul_template長輪詢監聽consul_server的配置變化
  • 有變化後,生成nginx配置,修改nginx配置,重啓nginx

好處是節點的註冊和摘除,能夠在管理後臺完成,不須要手動操做nginx配置和重啓。
缺點是上游服務須要有consul功能,不知道python有沒有,java有redis

方案二:consul+openResty

流程算法

  • 上游服務向consul_server註冊節點
  • 管理後臺向consul_server註冊或者摘除節點
  • nginx啓動後調用init_by_lua,想consul_server獲取配置
  • 而後nginx按期去consul_server拉取配置,而後reload

缺點是隻能按期,不能長鏈接,因此有延遲,解決方法是nginx暴露一個http api,開發一個agent,長輪詢監聽consul_server的配置變化,而後調用http api實時更新nginx的配置sql

感受上面兩個方案都有點蛋疼。。。。若是有msalt,能夠本身作個管理後臺,修改配置後,發送msalt任務,msalt本身修改nginx配置,而後reload數據庫

四層負載均衡

上面的都是http的負載均衡,那tcp鏈接,就要用四層負載均衡(7層網絡模型,tcp在第4層)

3、隔離

隔離是發生故障後,將故障服務和正常服務隔離,避免故障服務影響正常服務,形成滾雪球。

  1. 線程隔離
    1. 系統有兩個線程池,將核心業務請求導向線程池A,非核心的導向線程池B
    2. 這樣非核心業務的故障不會影響核心業務
  2. 進程隔離
    1. 跟線程相似
  3. 集羣隔離
    1. 對於一些壓力比較大的業務,例如秒殺,用一個單獨的集羣來實現,避免影響到其餘業務
  4. 機房隔離
    1. 當一個機房發生故障,把流量切到另外一個機房,實現高可用
  5. 讀寫隔離
    1. 例如redis或者mysql,讀請求走一個集羣,寫請求走另外一個集羣
  6. 動靜隔離
    1. 動是動態資源,靜是靜態資源。靜態資源儘可能放CDN
  7. 爬蟲隔離
    1. 識別爬蟲請求,導到單獨的集羣,避免影響正常請求
  8. 熱點隔離
    1. 例如秒殺,能夠用單獨集羣來實現
  9. 資源隔離
    19.資源是指硬件,例如CPU,磁盤,內存。 重要進程,單獨分配CPU資源,保證重要進程可用

Hystrix和Servlet3

都是java的組件,
隔離的思路是

  • 一臺機有多個線程池
  • 業務區分核心業務和非核心業務,分流到不一樣的線程池
  • 達到非核心業務過載或者一次,不會影響核心業務

4、限流

  • 限流算法
    • 令牌桶法
      • 進程A往桶裏塞令牌,例如速度是1s10個令牌,桶的容量有個上限,假如是100,溢出就丟棄
      • 請求來了,從桶裏獲取令牌(能夠根據請求的不一樣設置不一樣的令牌數,例如耗時的業務須要2個令牌,簡單的業務只須要1個)
      • 能獲取足夠的令牌,就處理請求
      • 不能,就丟棄請求,或者等待
    • 漏桶算法(有錯誤,須要從新整理)
      • 進程A以速率A流入水滴到桶裏
      • 若是溢出,就丟棄
      • 按照常量速率流出水滴
    • 感受兩個算法都很相似,都是相似生產消費的模式,來控制消費的速率
    • 令牌法容許突發流量,例如1s內把全部令牌都獲取完
    • 漏桶法不容許特發流量,並且配置的流出速率不能小於流入,否則就沒什麼意義了
  • 應用級限流(也就是在單臺機上面限流)
    • 限流總併發/鏈接/請求數 (例如某個時刻,若是併發數超過閾值,就丟棄請求)
    • 限制總資源數(資源通常指數據庫鏈接等)
    • 限制單個接口的總併發數/請求數(這個粒度就小一點)
    • 限制窗口時間的併發數,例如1s內,併發數不能大於N
    • 實現
      • 組件的限流功能,例如gevent的最大鏈接數
      • 本身用redis實現
        • 總併發數。key是接口名+機器ip,經過incr方法,若是小於N,執行,大於N丟棄,最後執行incr -1),而後設置超時時間
        • 窗口時間內總併發數。key是接口名+機器ip,經過incr方法,若是小於N,執行,大於N丟棄,而後設置超時時間(例如1s),這裏要考慮設置超時時間失敗,致使永遠釋放不了的問題,因此key最好帶上時間,
      • 能夠寫成一個裝飾器
  • 分佈式限流(相對於單機器,主要的難點是原子性)
    • 上面的redis也能夠實現分佈式限流
    • redis+lua 利用redis的incr實現原子性
    • Nginx+lua 利用lua的鎖來實現原子性(底層應該是信號量)
  • 接入層限流(Nginx的限流)
    • ngx_http_limit_conn_module
      • http {
          limit_conn_zone $binary_remote_addr zone=addr:10m; # 定義限流模塊addr 以ip地址做爲key;10m表示使用10m內存來進行ip傳輸;除了binary_remote_addr 表示ip外,server_name表示域名
          limit_conn_log_level error;  #限流日誌,觸發限流會打error日誌
          limit_conn_status 503;  #限流時返回的http code
          server {
              location /limit{
                  limit_conn addr 1; #定義addr模塊限流1
              }
          }
        }
    • ngx_http_limit_req_module(使用漏桶算法來實現)官方文檔
      • limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s
          location /limit{
          limit_req zone=one burst=5 nodelay;
          }
      • 其餘配置和limit_conn同樣
      • 這裏有三個參數:
        • 1r/s 表示每秒處理2個請求
        • burst=5 表示桶的容量=5
        • nodelay 非延遲,不加這個配置就是默認延遲
      • 狀況:
        • 狀況1 burst=0
          • 2r/s 表示500毫秒內,只能處理一個請求,多出的請求返回503。時間窗口是500毫秒。(時間窗口從何時開始,這個是nginx決定的,並且不是很是準確,可是這個影響不大)。例如時間窗口是05.000秒到05.500秒。若是在05.300處理了一個請求,下一個請求要到05.500秒,300-500內的請求會被拒絕
        • 狀況2 burst大於0,例如1,
          • 2r/s 表示500毫秒內,只能處理一個請求,超出的請求會放進桶裏,最多1個請求。超出桶容量的請求依然返回503。到下一個時間窗口,nginx會從桶裏取一個請求出來處理.因此假設3個請求在同一個時間窗口內到來,第一個會被正常處理,第二個會等待500毫秒再處理,第三個返回503
        • 狀況2 burst大於0,例如1,nodelay
          • 和狀況2同樣,不一樣的是,放進桶裏的請求會被馬上處理,而不是等下一個時間窗口。假設3個請求在同一個時間窗口內到來,第一個會被正常處理,第二個會放進桶裏,可是是馬上處理,不會等待,第三個返回503
    • 更高級的限流
      • 若是要更加複雜,更加了靈活的限流策略,就要用OpenResty,也就是用lua語言,寫限流策略,而後在Nginx上面執行
  • 節流
    • 節流是指在特定的時間窗口,對相同的請求,只處理一次。或者限制多個時間的執行間隔
    • 應用:
      • 例如防止用戶一直刷新頁面
    • 策略
      • throttleFirst,對於相同的請求,只處理第一個
      • throttleLast,對於相同的請求,只處理最後一個
      • throttleWithTimeout,限制兩個請求的間隔不能小於某個時間
    • 實現:
      • RxJava

5、降級

1.降級的分類

  • 從服務端鏈路考慮,能夠進行降級的地方有:
    • 頁面降級
    • 頁面片斷降級。例如商品詳情頁,不重要的信息不請求,例如商家信息
    • 讀降級
    • 寫降級
    • 爬蟲降級
    • 風控降級。識別用戶是否機器人,若是是,進行降級
  • 從讀和寫服務分類
    • 讀降級(包括上面的讀降級,頁面降級,頁面片斷降級等)
      • 降級方案:
        1. 返回緩存 。返回緩存裏的數據,或者返回上一次成功讀取的數據。這些數據不會很實時,因此適用於對數據一致性要求不高的場景。
        2. 返回默認值。例如讀取庫存的接口,默認返回沒貨。例如列表接口,默認返回空列表
        3. 返回兜底數據,例如列表接口返回靜態的幾個item,這些item通常寫在代碼裏面或者靜態文件裏面
    • 寫降級
      • 降級方案
        1. 異步。
          1. 先寫redis,異步寫DB。適用於須要判斷狀態的操做。例以下單,須要在redis判斷是否有緩存,若是有,寫入Redis,返回下單成功,若是沒有,返回失敗。若是成功,異步同步Redis到DB。
          2. 異步執行所有操做。適用於不須要判斷狀態的操做。例如寫評論,直接返回寫入成功。異步執行評論邏輯

2.降級的觸發

觸發分爲降級(打開降級開關)和降級後的恢復(關閉降級開噶)

  • 人工觸發。當開發人員經過監控或者告警,意識到系統須要降級時,手工打開降級開關,進行降級。當系統恢復時,手工關閉開關進行恢復
  • 自動降級。當系統經過某些指標,判斷系統須要降級時,自動打開降級開關。當系統判斷系統負載下降後,自動恢復
    • 降級指標有:
      1. 超時。當系統執行某個操做超時時,自動降級
      2. 失敗次數。當系統執行某個操做失敗次數超過N時,自動降級
      3. 故障。當系統執行某個操做失敗時,自動降級
      4. 限流。當某個服務觸發限流時,自動降級
    • 恢復
      • 時間窗口重試。當降級後,每一個時間窗口執行一次降級前的操做,若是成功,關閉降級開關。例如每1s執行一次操做。

3.降級的配置

降級開關,其實也就是一個配置,這個配置的實現能夠:

  • 代碼變量。也就是寫死在代碼裏面,修改配置須要修改代碼,而後重啓服務。
  • 配置中心。經過頁面就能夠修改配置,修改後能夠同步到全部機器
    • 開源方案:Zookeeper,Consul等
    • 實現方案,配置中心的難點是修改配置後,怎麼同步到多臺機器的多個進程裏面(一個進程裏面的多個線程或者協程,會共享一份配置)
      1. 定時更新。例如進程裏面每隔1s或者每100次讀取配置,就主動去配置中心更新最新的配置
      2. 監聽。當配置修改,經過IO多路複用機制通知進程去更新。相似於消息隊列,配置中心是生產者,進程是消費者。

4.降級的實現

書中介紹了使用Hystrix來實現降級

具體作法是

  1. 定義run和getFallback兩個函數,這個是不一樣業務不同的
  2. 當請求儘可能,先執行run函數,若是成功,就返回
  3. 若是失敗或者超時,進行降級,返回getFallback函數的內容

本身項目的作法

  • 對於每個接口,定義兩個函數,一個是未降級的邏輯run,另外一個是已降級的邏輯getFallback。
  • 封裝一層降級邏輯:
    • 若是降級開關關閉,執行run
    • 若是降級開關打開,執行getFallback
    • 若是run超時或者失敗,決定是否自動降級
    • 降級後 每一個時間窗口執行一次run,若是成功,決定是否恢復
    • getFallback能夠默認不定義

6、重試

代理層

代理層主要就是Nginx了

Nginx有有不少超時,或者重試的配置
下面的time是時間,例如能夠是5s

  • 客戶端超時配置
    • client_header_timeout time; nginx接收客戶端請求頭的超時時間
    • client_body_timeout time nginx接收客戶端請求體的超時時間
    • send_timeout time; 發送響應到客戶端的超時時間
    • keepalive_timeout timeout [header_timeout] 長鏈接的超時時間,header_timeout是返回給客戶端的,例如若是設置了,返回的響應頭就會有:Keep-Alive:timeout=10
      • 默認http1.1是打開長鏈接的,1.0不會,能夠經過wireshark來看看是否真的沒有3次握手
      • keepalive_requests 100 表示長鏈接能夠處理100次請求
  • 代理超時設置(代理就是上游的服務)
    • 鏈接超時:
      • proxy_connect_time time; 創建鏈接超時時間
      • proxy_read_timeout time ;從上游服務讀取響應的超時時間。注意不是讀取的超時時間,是發送請求後,到能夠讀取的超時時間
      • proxy_send_time time;創建鏈接後,發送請求,到上游開始接受請求的超時時間,注意不是開始接收請求到接收完請求的時間。
    • 失敗重試機制
      • proxy_next_upstream ;這個配置能夠是多個下面的選項
        • timeout 超時,包括創建鏈接,寫請求,讀響應頭的超時
        • invalid_header 上游服務返回錯誤響應頭
        • http_xxx,例如http_500,表示上游返回指定的httpcode
        • non_idempotent 非冪等請求(idempotent 是冪等的意思)。POST、LOCK、PATCH都是非冪等的請求。默認冪等的請求都是容許重試的。
        • off 關閉重試
      • proxy_next_upstream_tries number;失敗重試次數,包含第一次請求,也就是1表示不重試。0表示不限制。
      • proxy_next_upstream_timeout time;在此時間內執行重試,超事後就不重試了。0表示不限制

應用層

  • 超時
    • 設置好超時時間
      • 例如A調用B調用C,超時時間必定是A>B,否則會致使一直重試
      • 超時時間不能大於用戶的容忍時間,否則用戶本身會不斷重試
    • 超時後的策略是重試或者降級
  • 重試
    • 超時後通常的策略是重試
    • 非冪等的操做不能重試,最好設置全部操做都是冪等。

應用層上游

例如redis,mysql的鏈接都要設置好超時時間

7、回滾

  • 事務回滾
    • 若是是單機數據庫,執行rollback回滾就能夠了
    • 若是是分佈式事務
      • 補償機制
        • 回滾事務。例如扣優惠券成功了,可是下單事務失敗了,就把扣優惠券的事務回滾。這裏有有個問題就是萬一回滾前,進程掛了,就不能回滾了,因此要有個定時掃描機制,把未回滾的事務進行回滾
        • 重試。經過定時掃描機制,把失敗的事務進行重試。例如上面的下單事務
      • TCC事務。每一個事務分3步,Try-Confirm-Cancel。
        1. 扣優惠券,下單,都執行Try。例如優惠券的Try就是把要減的優惠券凍結
        2. Confirm。等Try都執行成功,執行Confirm,例如優惠券就是把凍結的優惠券扣掉
        3. Cancel。若是其中一個Try失敗,就執行Cancel,也就是回滾,例如把凍結的優惠券恢復
  • 代碼庫回滾,這些Git和Svn都很成熟了
  • 部署版本回滾。例如上線了新版本,可是有問題,須要回滾到舊版本
    • 部署前備份舊版本,作到能夠快速回滾
    • 灰度。

8、壓測

系統雪崩:整個系統所有不可用
雪崩效應:因爲一個小問題,致使整個系統不可用。例如整個系統都依賴於一個非核心業務,可是沒有作好降級,致使一旦這個業務不可用,致使全部核心業務都不可用。

壓測

  • 系統壓測,用來評估系統的穩定性和性能。經常使用指標有:
    • QPS/TPS T是事務
    • 響應時間,也就是時延
    • 機器負載
  • 壓測方式:
    • 線下壓測。也就是在非線上環境測試,例如測試環境。優勢是不用考慮正經常使用戶,缺點的壓測結果不夠真實
    • 線上壓測。在線上環境測試,這時要注意不要影響正經常使用戶,包括請求和數據。
    • 仿真壓測。模仿真實環境的訪問狀況(能夠經過access日誌來達到)。能夠對訪問量翻倍的方式增長壓力
    • 隔離集羣壓測,從線上集羣中摘除一臺機器,用來壓測
    • 導流壓測,把集羣的全部流量導到一臺機,風險比較大
    • 單機壓測,只在一臺機上面壓測,獲得單機的併發能力
    • 離散壓測,也就是不要只訪問熱點數據,由於熱點數據通常有緩存

系統優化和容災

壓測後,就知道系統的性能,就能根據預期負載來決定是否須要優化性能或者增長機器

應急預案

當上面兩步都作了,項目上線後,仍是會有一些突發狀況,對於這些突發狀況,須要作好預案。
一個預案須要有

  • 預案名稱
  • 問題描述(也就是遇到了什麼突發狀況)
  • 執行操做(遇到突發狀況怎麼處理)
  • 相關人員

例如

預案名稱 問題描述 執行操做 相關人員
機房故障 機房網絡不可用 DNS配置中摘掉該機房的IP 小A

高併發

9、緩存、HTTP緩存、多級緩存

主要講java的進程內緩存。可是如今基本都用redis緩存了,感受須要用進程內緩存的場景很少了。
瀏覽器中Ctrl+F5能夠強制刷新緩存

使用緩存能大幅提高系統的QPS,可是要注意下面幾點:

  • 緩存命中率,緩存設置了,可是大部分請求仍是去DB了
  • 緩存一致性,DB改了,緩存仍是舊數據
  • 緩存雪崩,多個緩存KEY一塊兒過時,致使請求都去DB了
  • 緩存穿透,緩存設置了,可是永遠用不了
  • 緩存更新。
    • 過時更新。緩存設置過時時間,若是過時,就回源。這裏有個坑是若是併發較大,並且回源的速度很慢,會致使多個請求同時回源,弄掛回源的服務。因此碎玉回源速度慢的業務,適用下面的定時更新
    • 定時任務更新。設置定時任務,定時回源,更新緩存。

多級緩存有(從用戶端到後端),緩存離用戶越近越好:

  • 瀏覽器緩存
  • CDN
  • 接入層緩存(Nginx+Lua+Redis)
  • 應用層緩存,基本是Redis

12、鏈接池,線程池

池化技術用於建設一些消耗,來提高性能。例如避免TCP鏈接和端口,避免線程建立和消耗

通常有指標:

  • 最小數量,當系統空閒時,最小維護的鏈接數量。數量太大會致使資源佔用較多,過小會起不到鏈接池的做用。通常這個數量乘以進程數,就是總的鏈接數。
  • 最大數量,當系統繁忙時,最大支持的鏈接數量,超過就等待,用來保護上游服務。

  • 上游服務主動關閉鏈接,例如Mysql通常8小時後會主動斷開空閒鏈接。因此業務端獲取鏈接池裏面的鏈接後,須要進行reconnect操做。能夠再獲取鏈接對象就檢查鏈接是否可用,也能夠在須要傳輸數據,也就是執行命令時,檢查是否可用。Redis是後者。
  • 等待超時時間,不知道是什麼意思,可是好像會遇到,也就是有大量TIMED_WAIT鏈接

十3、異步

書上說的異步是指處理多個IO請求的並行問題,因此這個叫並行合適點。

解決方法是從串行改成並行,可是就算改成並行,仍是須要阻塞一個線程,在java中,是有線程池的, 可是沒有協程,因此仍是會形成一個線程的浪費。

十4、擴容

  • 垂直擴容,例如換CPU,加內存,加硬盤等
  • 水平擴容,加機器
  • 應用拆分,把一個大系統拆分爲多個小系統,也就是微服務化。帶來的問題是分佈式事務,join查詢等。
  • 分庫分表
    • 分表
      • 當一個表太大,致使容量和磁盤/帶寬IO瓶頸(不是很明白)
    • 分庫
      • 當一臺機的性能不夠的時候,分爲多個庫。這裏的分庫是不一樣表分不分的庫。
    • 分庫分表
      • 分庫分表是原來的一張表,拆分爲多張子表,放在不一樣的庫。
    • 帶來的問題:
      • 查詢問題,因爲分表是按特定的一個字段(例如用戶ID)分的,若是需用用其餘維度來查詢,例如商品ID,就須要
        • 用合併表,或者另外一組分表
        • ES等搜索引擎
        • 上面兩種方法至關於數據冗餘,必然存在數據不一致請求,解決方法是1.消息隊列,通知對應的業務方更新數據 2. 監聽binlog日誌
      • 分佈式事務問題,這個上面有說

十5、隊列術

隊列的做用:

  • 異步處理,同步任務發送消息都隊列,另外一個進程從隊列獲取消息,處理異步任務。好處是:
    • 提高響應速度
    • 流量削峯
  • 系統解耦,系統1觸發一個事件,經過發消息隊列的方式,通知多個其餘系統。
  • 數據同步。系統1修改了數據,經過發消息隊列的方式,通知多個其餘系統。

十6、案例

主要講京東幾個業務的實現架構

相關文章
相關標籤/搜索