一文帶你深入了解 redis 複製技術及主從架構

主從架構可以說是互聯網必備的架構了,第一是為了保證服務的高可用,第二是為了實現讀寫分離,你可能熟悉我們常用的 MySQL 數據庫的主從架構,對於我們 redis 來說也不意外,redis 數據庫也有各種各樣的主從架構方式,在主從架構中會涉及到主節點與從節點之間的數據同步,這個數據同步的過程在 redis 中叫做複製,這在篇文章中,我們詳細的聊一聊 redis 的複製技術和主從架構 ,本文主要有以下內容:

  • 主從架構環境搭建
    • 主從架構的建立方式
    • 主從架構的斷開
  • 複製技術的原理
    • 數據同步過程
    • 心跳檢測
  • 主從拓撲架構
    • 一主一從
    • 一主多從
    • 樹狀結構

主從環境搭建

redis 的實例在默認的情況下都是主節點,所以我們需要修改一些配置來搭建主從架構,redis 的主從架構搭建還是比較簡單的,redis 提供了三種方式來搭建主從架構,在後面我們將就介紹,在介紹之前我們要先了解主從架構的特性:在主從架構中有一個主節點(master)和最少一個從節點(slave),並且數據複製是單向的,只能從主節點複製到從節點,不能由從節點到主節點。

主從架構的建立方式

主從架構的建立有以下三種方式:

  • 在 Redis.conf 配置文件中加入 slaveof {masterHost} {masterPort} 命令,隨 Redis 實例的啟動生效
  • 在 redis-server 啟動命令后加入 –slaveof {masterHost} {masterPort} 參數
  • 在 redis-cli 交互窗口下直接使用命令:slaveof {masterHost} {masterPort}

上面三種方式都可以搭建 Redis 主從架構,我們以第一種方式來演示,其他兩種方式自行嘗試,由於是演示,所以就在本地啟動兩個 Redis 實例,並不在多台機器上啟動 redis 的實例了,我們準備一個端口 6379 的主節點實例,準備一個端口 6480 從節點的實例,端口 6480 的 redis 實例配置文件取名為 6480.conf 並且在裏面添加 slaveof 語句,在配置文件最後加入如下一條語句

slaveof 127.0.0.1 6379

分別啟動兩個 redis 實例,啟動之後他們會自動建立主從關係,關於這背後的原理,我們後面在詳細的聊一聊,先來驗證一下我們的主從架構是否搭建成功,我們先在 6379 master 節點上新增一條數據:

然後再 6480 slave 節點上獲取該數據:

可以看出我們在 slave 節點上已經成功的獲取到了在 master 節點新增的值,說明主從架構已經搭建成功了,我們使用 info replication 命令來查看兩個節點的信息,先來看看主節點的信息

可以看出 6379 端口的實例 role 為 master,有一個正在連接的實例,還有其他運行的信息,我們再來看看 6480 端口的 redis 實例信息

可以看出兩個節點之間相互記錄著對象的信息,這些信息在數據複製時候將會用到。在這裡有一點需要說明一下,默認情況下 slave 節點是只讀的,並不支持寫入,也不建議開啟寫入,我們可以驗證一下,在 6480 實例上寫入一條數據

127.0.0.1:6480> set x 3
(error) READONLY You can't write against a read only replica.
127.0.0.1:6480> 

提示只讀,並不支持寫入操作,當然我們也可以修改該配置,在配置文件中 replica-read-only yes 配置項就是用來控制從服務器只讀的,為什麼只能只讀?因為我們知道複製是單向的,數據只能由 master 到 slave 節點,如果在 salve 節點上開啟寫入的話,那麼修改了 slave 節點的數據, master 節點是感知不到的,slave 節點的數據並不能複製到 master 節點上,這樣就會造成數據不一致的情況,所以建議 slave 節點只讀。

主從架構的斷開

主從架構的斷開同樣是 slaveof 命令,在從節點上執行 slaveof no one 命令就可以與主節點斷開追隨關係,我們在 6480 節點上執行 slaveof no one 命令

127.0.0.1:6480> slaveof no one
OK
127.0.0.1:6480> info replication
# Replication
role:master
connected_slaves:0
master_replid:a54f3ba841c67762d6c1e33456c97b94c62f6ac0
master_replid2:e5c1ab2a68064690aebef4bd2bd4f3ddfba9cc27
master_repl_offset:4367
second_repl_offset:4368
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:4367
127.0.0.1:6480> 

執行完 slaveof no one 命令之後,6480 節點的角色立馬恢復成了 master ,我們再來看看時候還和 6379 實例連接在一起,我們在 6379 節點上新增一個 key-value

127.0.0.1:6379> set y 3
OK

在 6480 節點上 get y

127.0.0.1:6480> get y
(nil)
127.0.0.1:6480> 

在 6480 節點上獲取不到 y ,因為 6480 節點已經跟 6379 節點斷開的聯繫,不存在主從關係了,slaveof 命令不僅能夠斷開連接,還能切換主服務器,使用命令為 slaveof {newMasterIp} {newMasterPort},我們讓 6379 成為 6480 的從節點, 在 6379 節點上執行 slaveof 127.0.0.1 6480 命令,我們在來看看 6379 的 info replication

127.0.0.1:6379> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6480
master_link_status:up
master_last_io_seconds_ago:2
master_sync_in_progress:0
slave_repl_offset:4367
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:99624d4b402b5091552b9cb3dd9a793a3005e2ea
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:4367
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:4368
repl_backlog_histlen:0
127.0.0.1:6379> 

6379 節點的角色已經是 slave 了,並且主節點的是 6480 ,我們可以再看看 6480 節點的 info replication

127.0.0.1:6480> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6379,state=online,offset=4479,lag=1
master_replid:99624d4b402b5091552b9cb3dd9a793a3005e2ea
master_replid2:a54f3ba841c67762d6c1e33456c97b94c62f6ac0
master_repl_offset:4479
second_repl_offset:4368
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:4479
127.0.0.1:6480> 

在 6480 節點上有 6379 從節點的信息,可以看出 slaveof 命令已經幫我們完成了主服務器的切換。

複製技術的原理

redis 的主從架構好像很簡單一樣,我們就執行了一條命令就成功搭建了主從架構,並且數據複製也沒有問題,使用起來確實簡單,但是這背後 redis 還是幫我們做了很多的事情,比如主從服務器之間的數據同步、主從服務器的狀態檢測等,這背後 redis 是如何實現的呢?接下來我們就一起看看

數據複製原理

我們執行完 slaveof 命令之後,我們的主從關係就建立好了,在這個過程中, master 服務器與 slave 服務器之間需要經歷多個步驟,如下圖所示:

slaveof 命令背後,主從服務器大致經歷了七步,其中權限驗證這一步不是必須的,為了能夠更好的理解這些步驟,就以我們上面搭建的 redis 實例為例來詳細聊一聊各步驟。

1、保存主節點信息

在 6480 的客戶端向 6480 節點服務器發送 slaveof 127.0.0.1 6379 命令時,我們會立馬得到一個 OK

127.0.0.1:6480> slaveof 127.0.0.1 6379
OK
127.0.0.1:6480> 

這時候數據複製工作並沒有開始,數據複製工作是在返回 OK 之後才開始執行的,這時候 6480 從節點做的事情是將給定的主服務器 IP 地址 127.0.0.1 以及端口 6379 保存到服務器狀態的 masterhost 屬性和 masterport 屬性裏面

2、建立 socket 連接

在 slaveof 命令執行完之後,從服務器會根據命令設置的 IP 地址和端口,跟主服務器創建套接字連接, 如果從服務器能夠跟主服務器成功的建立 socket 連接,那麼從服務器將會為這個 socket 關聯一個專門用於處理複製工作的文件事件處理器,這個處理器將負責後續的複製工作,比如接受全量複製的 RDB 文件以及服務器傳來的寫命令。同樣主服務器在接受從服務器的 socket 連接之後,將為該 socket 創建一個客戶端狀態,這時候的從服務器同時具有服務器和客戶端兩個身份,從服務器可以向主服務器發送命令請求而主服務器則會向從服務器返回命令回復。

3、發送 ping 命令

從服務器與主服務器連接成功后,做的第一件事情就是向主服務器發送一個 ping 命令,發送 ping 命令主要有以下目的:

  • 檢測主從之間網絡套接字是否可用
  • 檢測主節點當前是否可接受處理命令

在發送 ping 命令之後,正常情況下主服務器會返回 pong 命令,接受到主服務器返回的 pong 回復之後就會進行下一步工作,如果沒有收到主節點的 pong 回復或者超時,比如網絡超時或者主節點正在阻塞無法響應命令,從服務器會斷開複製連接,等待下一次定時任務的調度。

4、身份驗證

從服務器在接收到主服務器返回的 pong 回復之後,下一步要做的事情就是根據配置信息決定是否需要身份驗證:

  • 如果從服務器設置了 masterauth 參數,則進行身份驗證
  • 如果從服務器沒有設置 masterauth 參數,則不進行身份驗證

在需要身份驗證的情況下,從服務器將就向主服務器發送一條 auth 命令,命令參數為從服務器 masterauth 選項的值,舉個例子,如果從服務器的配置里將 masterauth 參數設置為:123456,那麼從服務器將向主服務器發送 auth 123456 命令,身份驗證的過程也不是一帆風順的,可能會遇到以下幾種情況:

  • 從服務器通過 auth 命令發送的密碼與主服務器的 requirepass 參數值一致,那麼將繼續進行後續操作,如果密碼不一致,主服務將返回一個 invalid password 錯誤
  • 如果主服務器沒有設置 requirepass 參數,那麼主服務器將返回一個 no password is set 錯誤

所有的錯誤情況都會令從服務器中止當前的複製工作,並且要從建立 socket 開始重新發起複制流程,直到身份驗證通過或者從服務器放棄執行複製為止

5、發送端口信息

在身份驗證通過後,從服務器將執行 REPLCONF listening 命令,向主服務器發送從服務器的監聽端口號,例如在我們的例子中從服務器監聽的端口為 6480,那麼從服務器將向主服務器發送 REPLCONF listening 6480 命令,主服務器接收到這個命令之後,會將端口號記錄在從服務器所對應的客戶端狀態的 slave_listening_port 屬性了,也就是我們在 master 服務器的 info replication 裏面看到的 port 值。

6、數據複製

數據複製是最複雜的一塊了,由 psync 命令來完成,從服務器會向主服務器發送一個 psync 命令來進行數據同步,在 redis 2.8 版本以前使用的是 sync 命令,除了命令不同之外,在複製的方式上也有很大的不同,在 redis 2.8 版本以前使用的都是全量複製,這對主節點和網絡會造成很大的開銷,在 redis 2.8 版本以後,數據同步將分為全量同步和部分同步。

  • 全量複製:一般用於初次複製場景,不管是新舊版本的 redis 在從服務器第一次與主服務連接時都將進行一次全量複製,它會把主節點的全部數據一次性發給從節點,當數據較大時,會對主節點和網絡造成很大的開銷,redis 的早期版本只支持全量複製,這不是一種高效的數據複製方式

  • 部分複製:用於處理在主從複製中因網絡閃斷等原因造成的數據丟失 場景,當從節點再次連上主節點后,如果條件允許,主節點會補發丟失數據 給從節點。因為補發的數據遠遠小於全量數據,可以有效避免全量複製的過高開銷,部分複製是對老版複製的重大優化,有效避免了不必要的全量複製操作

redis 之所以能夠支持全量複製和部分複製,主要是對 sync 命令的優化,在 redis 2.8 版本以後使用的是一個全新的 psync 命令,命令格式為:psync {runId} {offset},這兩個參數的意義:

  • runId:主節點運行的id
  • offset:當前從節點複製的數據偏移量

也許你對上面的 runid、offset 比較陌生,沒關係,我們先來看看下面三個概念:

1、複製偏移量

參与複製的主從節點都會分別維護自身複製偏移量:主服務器每次向從服務器傳播 N 個字節的數據時,就將自己的偏移量的值加上 N,從服務器每次接收到主服務器傳播的 N個字節的數據時,將自己的偏移量值加上 N。通過對比主從服務器的複製偏移量,就可以知道主從服務器的數據是否一致,如果主從服務器的偏移量總是相同,那麼主從數據一致,相反,如果主從服務器兩個的偏移量並不相同,那麼說明主從服務器並未處於數據一致的狀態,比如在有多個從服務器時,在傳輸的過程中某一個服務器離線了,如下圖所示:

由於從服務器A 在數據傳輸時,由於網絡原因掉線了,導致偏移量與主服務器不一致,那麼當從服務器A 重啟並且與主服務器連接成功后,重新向主服務器發送 psync 命令,這時候數據複製應該執行全量複製還是部分複製呢?如果執行部分複製,主服務器又如何補償從服務器A 在斷線期間丟失的那部分數據呢?這些問題的答案都在複製積壓緩衝區裏面

2、複製積壓緩衝區

複製積壓緩衝區是保存在主節點上的一個固定長度的隊列,默認大小為 1MB,當主節點有連接的從節點(slave)時被創建,這時主節點(master) 響應寫命令時,不但會把命令發送給從節點,還會寫入複製積壓緩衝區,如下圖所示:

因此,主服務器的複製積壓緩衝區裏面會保存着一部分最近傳播的寫命令,並且複製積壓緩衝區會為隊列中的每個字節記錄相應的複製偏移量。所以當從服務器重新連上主服務器時,從服務器通過 psync 命令將自己的複製偏移量 offset 發送給主服務器,主服務器會根據這個複製偏移量來決定對從服務器執行何種數據同步操作:

  • 如果從服務器的複製偏移量之後的數據仍然存在於複製積壓緩衝區裏面,那麼主服務器將對從服務器執行部分複製操作
  • 如果從服務器的複製偏移量之後的數據不存在於複製積壓緩衝區裏面,那麼主服務器將對從服務器執行全量複製操作

3、服務器運行ID

每個 Redis 節點啟動后都會動態分配一個 40 位的十六進制字符串作為運行 ID,運行 ID 的主要作用是用來唯一識別 Redis 節點,我們可以使用 info server 命令來查看

127.0.0.1:6379> info server
# Server
redis_version:5.0.5
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:2ef1d58592147923
redis_mode:standalone
os:Linux 3.10.0-957.27.2.el7.x86_64 x86_64
arch_bits:64
multiplexing_api:epoll
atomicvar_api:atomic-builtin
gcc_version:4.8.5
process_id:25214
run_id:7b987673dfb4dfc10dd8d65b9a198e239d20d2b1
tcp_port:6379
uptime_in_seconds:14382
uptime_in_days:0
hz:10
configured_hz:10
lru_clock:14554933
executable:/usr/local/redis-5.0.5/src/./redis-server
config_file:/usr/local/redis-5.0.5/redis.conf
127.0.0.1:6379> 

這裏面有一個run_id 字段就是服務器運行的ID

了解這幾個概念之後,我們一起來看看 psync 命令的運行流程,psync 命令運行流程如下圖所示:

psync 命令的邏輯比較簡單,整個流程分為兩步:

1、從節點發送 psync 命令給主節點,參數 runId 是當前從節點保存的主節點運行ID,參數offset是當前從節點保存的複製偏移量,如果是第一次參与複製則默認值為 -1。

2、主節點接收到 psync 命令之後,會向從服務器返回以下三種回復中的一種:

  • 回復 +FULLRESYNC {runId} {offset}:表示主服務器將與從服務器執行一次全量複製操作,其中 runid 是這個主服務器的運行 id,從服務器會保存這個id,在下一次發送 psync 命令時使用,而 offset 則是主服務器當前的複製偏移量,從服務器會將這個值作為自己的初始化偏移量
  • 回復 +CONTINUE:那麼表示主服務器與從服務器將執行部分複製操作,從服務器只要等着主服務器將自己缺少的那部分數據發送過來就可以了
  • 回復 +ERR:那麼表示主服務器的版本低於 redis 2.8,它識別不了 psync 命令,從服務器將向主服務器發送 sync 命令,並與主服務器執行全量複製

7、命令持續複製

當主節點把當前的數據同步給從節點后,便完成了複製的建立流程。但是主從服務器並不會斷開連接,因為接下來主節點會持續地把寫命令發送給從節點,保證主從數據一致性。

經過上面 7 步就完成了主從服務器之間的數據同步,由於這篇文章的篇幅比較長,關於全量複製和部分複製的細節就不介紹了,全量複製就是將主節點的當前的數據生產 RDB 文件,發送給從服務器,從服務器再從本地磁盤加載,這樣當文件過大時就需要特別大的網絡開銷,不然由於數據傳輸比較慢會導致主從數據延時較大,部分複製就是主服務器將複製積壓緩衝區的寫命令直接發送給從服務器。

心跳檢測

心跳檢測是發生在主從節點在建立複製后,它們之間維護着長連接並彼此發送心跳命令,便以後續持續發送寫命令,主從心跳檢測如下圖所示:

主從節點彼此都有心跳檢測機制,各自模擬成對方的客戶端進行通信,主從心跳檢測的規則如下:

  • 主節點默認每隔 10 秒對從節點發送 ping 命令,判斷從節點的存活性和連接狀態。可通過修改 redis.conf 配置文件裏面的 repl-ping-replica-period 參數來控制發送頻率
  • 從節點在主線程中每隔 1 秒發送 replconf ack {offset} 命令,給主節點 上報自身當前的複製偏移量,這條命令除了檢測主從節點網絡之外,還通過發送複製偏移量來保證主從的數據一致

主節點根據 replconf 命令判斷從節點超時時間,體現在 info replication 統 計中的 lag 信息中,我們在主服務器上執行 info replication 命令:

127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6480,state=online,offset=25774,lag=0
master_replid:c62b6621e3acac55d122556a94f92d8679d93ea0
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:25774
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:25774
127.0.0.1:6379> 

可以看出 slave0 字段的值最後面有一個 lag,lag 表示與從節點最後一次通信延遲的秒數,正常延遲應該在 0 和 1 之間。如果超過 repl-timeout 配置的值(默認60秒),則判定從節點下線並斷開複製客戶端連接,如果從節點重新恢復,心跳檢測會繼續進行。

主從拓撲架構

Redis的主從拓撲結構可以支持單層或多層複製關係,根據拓撲複雜性可以分為以下三種:一主一從、一主多從、樹狀主從架構

一主一從結構

一主一從結構是最簡單的複製拓撲結構,我們前面搭建的就是一主一從的架構,架構如圖所示:

一主一從架構用於主節點出現宕機時從節點 提供故障轉移支持,當應用寫命令併發量較高且需要持久化時,可以只在從節點上開啟 AOF,這樣既保證數據安全性同時也避免了持久化對主節點的性能干擾。但是這裡有一個坑,需要你注意,就是當主節點關閉持久化功能時, 如果主節點脫機要避免自動重啟操作。因為主節點之前沒有開啟持久化功能自動重啟后數據集為空,這時從節點如果繼續複製主節點會導致從節點數據也被清空的情況,喪失了持久化的意義。安全的做法是在從節點上執行 slaveof no one 斷開與主節點的複製關係,再重啟主節點從而避免這一問題

一主多從架構

一主多從架構又稱為星形拓撲結構,一主多從架構如下圖所示:

一主多從架構可以實現讀寫分離來減輕主服務器的壓力,對於讀佔比較大的場景,可以把讀命令發送到 從節點來分擔主節點壓力。同時在日常開發中如果需要執行一些比較耗時的讀命令,如:keys、sort等,可以在其中一台從節點上執行,防止慢查詢對主節點造成阻塞從而影響線上服務的穩定性。對於寫併發量較高的場景,多個從節點會導致主節點寫命令的多次發送從而過度消耗網絡帶寬,同時也加重了主節點的負載影響服務穩定性。

樹狀主從架構

樹狀主從架構又稱為樹狀拓撲架構,樹狀主從架構如下圖所示:

樹狀主從架構使得從節點不但可以複製主節 數據,同時可以作為其他從節點的主節點繼續向下層複製。解決了一主多從架構中的不足,通過引入複製中 間層,可以有效降低主節點負載和需要傳送給從節點的數據量。如架構圖中,數據寫入節點A 後會同步到 B 和 C節點,B節點再把數據同步到 D 和 E節點,數據實現了一層一層的向下複製。當主節點需要掛載多個從節點時為了避免對主節點的性能干擾,可以採用樹狀主從結構降低主節點壓力。

最後

目前互聯網上很多大佬都有 Redis 系列教程,如有雷同,請多多包涵了。原創不易,碼字不易,還希望大家多多支持。若文中有所錯誤之處,還望提出,謝謝。

歡迎掃碼關注微信公眾號:「平頭哥的技術博文」,和平頭哥一起學習,一起進步。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

※收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

※網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品在網路上成為最夯、最多人討論的話題?

※高價收購3C產品,價格不怕你比較

※想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師”嚨底家”!!

看完這篇還不會用Git,那我就哭了!

你使用過 Git 嗎?也許你已經使用了一段時間,但它的許多奧秘仍然令人困惑。

Git 是一個版本控制系統,是任何軟件開發項目中的主要內容。通常有兩個主要用途:代碼備份和代碼版本控制。你可以逐步處理代碼,在需要回滾到備份副本的過程中保存每一步的進度!

常見的問題是 Git 很難使用。有時版本和分支不同步,你會花很長時間試圖推送代碼!更糟糕的是,不知道某些命令的確切工作方式很容易導致意外刪除或覆蓋部分代碼!

這就是我寫本文的原因,從而學習到如何正確使用 Git,以便在開發中共同進行編碼!

安裝和配置

Git 安裝

首先,我們必須安裝 Git 才能使用它!這裏分 Linux 和 Windows 來演示:

在 Linux 上安裝 Git

我們可以使用 yum 輕鬆快速地做到這一點:

sudo yum install git

在 Windows 上安裝 Git

直接在 https://git-scm.com/downloads 裏面,下載最新版的 Git,默認安裝就可以了。

安裝完成后,在開始菜單里找到 Git->Git Bash,點擊后出現一個類似命令行窗口的東西,就說明 Git 安裝成功。

Git 配置

可以保存 Git 用戶名和电子郵件,這樣就不必在以後的 Git 命令中再次輸入它們。

在命令行中配置本地倉庫的賬號和郵箱:

$ git config --global user.name "wupx"  
$ git config --global user.email "wupx@qq.com"  

好多人都不知道的小技巧是,你可以為 Git 啟用一些額外的顏色,這樣就可以更容易地閱讀命令的輸出!

git config --global color.ui true

Git 基本版本控制

初始化 Git

現在,我們可以開始對項目進行版本控制。使用 cd 命令導航到要在終端中設置版本控制的目錄,現在你可以像這樣初始化 Git 存儲庫:

git init

這將創建一個名為 .git 的新子目錄(Windows 下該目錄為隱藏的),其中包含所有必需的存儲庫文件(Git 存儲庫框架)。至此,你的項目中尚未跟蹤任何內容。

添加並提交

要開始對現有文件進行版本控制,你應該先跟蹤這些文件並進行初始提交。要做到這一點,你首先需要將文件添加到 Git 中,並將它們附加到 Git 項目中。

git add <file>
git commit -m 'first commit'

遠程備份

很棒!你現在已經開始在本地對項目進行版本控制。如果你想遠程保存和備份項目,則需要在 GitHub 上創建一個遠程存儲庫(它是免費的!)。因此,首先轉到 github.com 並創建一個存儲庫。然後,使用存儲庫的鏈接將其添加為本地 git 項目的來源,即該代碼的存儲位置。

# 示例
git remote add origin \
https://github.com/wupeixuan/repo.git 
# 以我的一個倉庫為例
git remote add origin \
https://github.com/wupeixuan/JDKSourceCode1.8.git

然後,你可以繼續將代碼推送到 GitHub!哇,你已經成功備份了你的代碼!

git push origin master

處理文件

狀態檢查

git status 命令用於確定哪些文件處於哪種狀態,它使你可以查看哪些文件已提交,哪些文件尚未提交。如果在所有文件都已提交並推送后運行此命令,則應該看到類似以下內容:

$ git status
# On branch master
nothing to commit (working directory clean)

如果你將新文件添加到項目中,而該文件之前不存在,則在運行 git status 時,你應該看到未跟蹤的文件,如下所示:

$ git status
# On branch master
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#   README
nothing added to commit but untracked files present (use "git add" to track)

使用 git status 對於快速檢查你已經備份的內容和你僅在本地擁有的內容非常有用。

高級文件添加

還有一些更高級的方法可以將文件添加到 Git 中,從而使你的工作流程更高效。我們可以執行以下操作,而不是試圖查找所有有更改的文件並逐個添加它們:

# 逐個添加文件
git add filename

# 添加當前目錄中的所有文件
git add -A

# 添加當前目錄中的所有文件更改
git add .

# 選擇要添加的更改(你可以 Y 或 N 完成所有更改)
git add -p

高級提交

我們可以使用 git commit -m '提交信息' 來將文件提交到 Git。對於提交簡短消息來說,這一切都很好,但是如果你想做一些更精細的事情,你需要來學習更多的操作:

### 提交暫存文件,通常用於較短的提交消息
git commit -m 'commit message'

### 添加文件並提交一次
git commit filename -m 'commit message'

### 添加文件並提交暫存文件
git commit -am 'insert commit message'

### 更改你的最新提交消息
git commit --amend 'new commit message' 

# 將一系列提交合併為一個提交,你可能會用它來組織混亂的提交歷史記錄
git rebase -i
### 這將為你提供核心編輯器上的界面:
# Commands:
#  p, pick = use commit
#  r, reword = use commit, but edit the commit message
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#  f, fixup = like "squash", but discard this commit's log message
#  x, exec = run command (the rest of the line) using shell

分支與合併

GitHub存儲庫的master分支應始終包含有效且穩定的代碼。但是,你可能還希望備份一些當前正在處理的代碼,但這些代碼並不完全穩定。也許你要添加一個新功能,你正在嘗試和破壞很多代碼,但是你仍然希望保留備份以保存進度!

分支使你可以在不影響master分支的情況下處理代碼的單獨副本。首次創建分支時,將以新名稱創建master分支的完整克隆。然後,你可以獨立地在此新分支中修改代碼,包括提交文件等。一旦你的新功能已完全集成並且代碼穩定,就可以將其合併到master分支中!

分支

這是你在分支上創建和工作所需的所有東西:

### 創建一個本地分支
git checkout -b branchname

### 在2個分支之間切換
git checkout prc/dev-wupx
git checkout master

### 將新的本地分支作為備份
git push -u origin branch_2

### 刪除本地分支,這不會讓你刪除尚未合併的分支
git branch -d branch_2

### 刪除本地分支,即使尚未合併,這也會刪除該分支!
git branch -D branch_2

### Viewing all current branches for the repository, including both ### local and remote branches. Great to see if you already have a ### branch for a particular feature addition, especially on bigger ### projects
### 查看存儲庫的所有當前分支,包括本地和遠程分支。
git branch -a

### 查看已合併到您當前分支中的所有分支,包括本地和遠程。 非常適合查看所有代碼的來源!
git branch -a --merged

### 查看尚未合併到當前分支中的所有分支,包括本地和遠程
git branch -a --no-merged

### 查看所有本地分支
git branch

### 查看所有遠程分支
git branch -r

# 將主分支重新設置為本地分支
$ git rebase origin/master

# 將分支推送到遠程存儲庫源並對其進行跟蹤
$ git push origin branchname

合併

很棒!現在,你已經學習了如何創建分支並開始敲代碼!將新功能添加到分支中之後,你需要將其合併回master分支,以便您的master具有所有最新的代碼功能。

方法如下:

### 首先確保你正在查看 master 分支
git checkout master

### 現在將你的分支合併到 master 
git merge prc/dev-wupx

你可能必須修复分支與主服務器之間的任何代碼衝突,但是 Git 將向你展示在鍵入該 merge 命令后如何執行所有這些操作。

修復錯誤和回溯

發生錯誤……它們經常在編碼中發生!重要的是我們能夠修復它們。

不要慌!Git 提供了你所需的一切,以防你在所推送的代碼中犯錯,改寫某些內容或者只是想對所推送的內容進行更正。

### 切換到最新提交的代碼版本
git reset HEAD 
git reset HEAD -- filename # for a specific file
### 切換到最新提交之前的代碼版本
git reset HEAD^ -- filename
git reset HEAD^ -- filename # for a specific file
### 切換回3或5次提交
git reset HEAD~3 -- filename
git reset HEAD~3 -- filename # for a specific file
git reset HEAD~5 -- filename
git reset HEAD~5 -- filename # for a specific file
### 切換回特定的提交,其中 0766c053 為提交 ID
git reset 0766c053 -- filename
git reset 0766c053 -- filename # for a specific file
### 先前的命令是所謂的軟重置。 你的代碼已重置,但是git仍會保留其他代碼的副本,以備你需要時使用。 另一方面,--hard 標誌告訴Git覆蓋工作目錄中的所有更改。
git reset --hard 0766c053

對 Git 有用的提示和技巧

我們已經完成了所有細節部分!以下是一些 Git 提示和技巧,你可能會發現它們對改善工作流程非常有用!

搜索

### 搜索目錄中的字符串部分
git grep 'project'

### 在目錄中搜索部分字符串,-n 打印出 git 找到匹配項的行號
git grep -n 'project'

### git grep -C <行數> 'something' 搜索帶有某些上下文的字符串部分(某些行在我們正在尋找的字符串之前和之後)
git grep -C<number of lines> 'project'

### 搜索字符串的一部分,並在字符串之前显示行
git grep -B<number of lines> 'project'

### 搜索字符串的一部分,並在字符串之後显示行
git grep -A<number of lines> 'something'

看誰寫了什麼

### 显示帶有作者姓名的文件的更改歷史記錄
git blame 'filename'

### 显示帶有作者姓名和 git commit ID 的文件的更改歷史記錄
git blame 'filename' -l

日誌

### 显示存儲庫中所有提交的列表 該命令显示有關提交的所有信息,例如提交ID,作者,日期和提交消息
git log

### 提交列表僅显示提交消息和更改
git log -p

### 包含您要查找的特定字符串的提交列表
git log -S 'project'

### 作者提交的清單
git log --author 'wupx'

### 显示存儲庫中提交列表的摘要。显示提交ID和提交消息的較短版本。
git log --oneline

### 显示昨天以來倉庫中的提交列表
git log --since=yesterday

### 显示作者日誌,並在提交消息中搜索特定術語
git log --grep "project" --author "wupx"

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

基於 HTML5 WebGL 和 VR 技術的 3D 機房數據中心可視化

 

前言

在 3D 機房數據中心可視化應用中,隨着視頻監控聯網系統的不斷普及和發展, 網絡攝像機更多的應用於監控系統中,尤其是高清時代的來臨,更加快了網絡攝像機的發展和應用。

在監控攝像機數量的不斷龐大的同時,在監控系統中面臨着嚴峻的現狀問題:海量視頻分散、孤立、視角不完整、位置不明確等問題,始終圍繞着使用者。因此,如何更直觀、更明確的管理攝像機和掌控視頻動態,已成為提升視頻應用價值的重要話題。所以當前項目正是從解決此現狀問題的角度,應運而生。圍繞如何提高、管理和有效利用前端設備採集的海量信息為公共安全服務,特別是在技術融合大趨勢下,如何結合當前先進的視頻融合,虛實融合、三維動態等技術,實現三維場景實時動態可視化監控,更有效的識別、分析、挖掘海量數據的有效信息服務公共應用,已成為視頻監控平台可視化發展的趨勢和方向。目前,在監控行業中,海康、大華等做監控行業領導者可基於這樣的方式規劃公共場所園區等的攝像頭規劃安放布局,可以通過海康、大華等攝像頭品牌的攝像頭參數,調整系統中攝像頭模型的可視範圍,監控方向等,更方便的讓人們直觀的了解攝像頭的監控區域,監控角度等。

以下是項目地址:

效果預覽

整體場景-攝像頭效果圖

局部場景-攝像頭效果圖

代碼生成
攝像頭模型及場景

項目中使用的攝像頭模型是通過 3dMax 建模生成的,該建模工具可以導出 obj 與 mtl 文件,在 HT 中可以通過解析 obj 與 mtl 文件來生成 3d 場景中的攝像頭模型。

項目中場景通過 HT 的 3d 編輯器進行搭建,場景中的模型有些是通過 HT 建模,有些通過 3dMax 建模,之後導入 HT 中,場景中的地面白色的燈光,是通過 HT 的 3d 編輯器進行地面貼圖呈現出來的效果。

錐體建模

3D 模型是由最基礎的三角形面拼接合成,例如 1 個矩形可以由 2 個三角形構成,1 個立方體由 6 個面即 12 個三角形構成, 以此類推更複雜的模型可以由許多的小三角形組合合成。因此 3D 模型定義即為對構造模型的所有三角形的描述, 而每個三角形由三個頂點 vertex 構成, 每個頂點 vertex 由 x, y, z 三維空間坐標決定,HT 採用右手螺旋定則來確定三個頂點構造三角形面的正面。

HT 中通過 ht.Default.setShape3dModel(name, model) 函數,可註冊自定義 3D 模型,攝像頭前方生成的錐體便是通過該方法生成。可以將該錐體看成由 5 個頂點,6 個三角形組成,具體圖如下:

ht.Default.setShape3dModel(name, model)

1. name 為模型名稱,如果名稱與預定義的一樣,則會替換預定義的模型 
2. model 為JSON類型對象,其中 vs 表示頂點坐標數組,is 表示索引數組,uv 表示貼圖坐標數組,如果想要單獨定義某個面,可以通過 bottom_vs,bottom_is,bottom_uv,top_vs,top_is, top_uv 等來定義,之後便可以通過 shape3d.top.*, shape3d.bottom.*  等單獨控制某個面

以下是我定義模型的代碼:

// camera 是當前的攝像頭圖元
// fovy 為攝像頭的張角的一半的 tan 值
var setRangeModel = function(camera, fovy) {
    var fovyVal = 0.5 * fovy; var pointArr = [0, 0, 0, -fovyVal, fovyVal, 0.5, fovyVal, fovyVal, 0.5, fovyVal, -fovyVal, 0.5, -fovyVal, -fovyVal, 0.5]; ht.Default.setShape3dModel(camera.getTag(), [{ vs: pointArr, is: [2, 1, 0, 4, 1, 0, 4, 3, 0, 3, 2, 0], from_vs: pointArr.slice(3, 15), from_is: [3, 1, 0, 3, 2, 1], from_uv: [0, 0, 1, 0, 1, 1, 0, 1] }]); }

我將當前攝像頭的 tag 標籤值作為模型的名稱,tag 標籤在 HT 中用於唯一標識一個圖元,用戶可以自定義 tag 的值。通過 pointArr 記錄當前五面體的五個頂點坐標信息,代碼中通過 from_vs, from_is, from_uv 單獨構建五面體底面,底面用於显示當前攝像頭呈現的圖像。

代碼中設置了錐體 style 對象的 wf.geometry 屬性,通過該屬性可以為錐體添加模型的線框,增強模型的立體效果,並且通過 wf.color,wf.width 等參數調節線框的顏色,粗細等。

相關模型 style 屬性的設置代碼如下:

 1 rangeNode.s({
 2     'shape3d': cameraName, 3 // 攝像頭模型名稱 4 'shape3d.color': 'rgba(52, 148, 252, 0.3)', 5 // 錐體模型顏色 6 'shape3d.reverse.flip': true, 7 // 錐體模型的反面是否显示正面的內容 8 'shape3d.light': false, 9 // 錐體模型是否受光線影響 10 'shape3d.transparent': true, 11 // 錐體模型是否透明 12 '3d.movable': false, 13 // 錐體模型是否可移動 14 'wf.geometry': true // 是否显示錐體模型線框 15 });

攝像頭圖像生成原理

透視投影

透視投影是為了獲得接近真實三維物體的視覺效果而在二維的紙或者畫布平面上繪圖或者渲染的一種方法,它也稱為透視圖。 透視使得遠的對象變小,近的對象變大,平行線會出現先交等更更接近人眼觀察的視覺效果。

如上圖所示,透視投影最終显示到屏幕上的內容只有截頭錐體( View Frustum )部分的內容, 因此 Graph3dView 提供了 eye, center, up, far,near,fovy 和 aspect 參數來控制截頭錐體的具體範圍。具體的透視投影可以參考 HT for Web 的  手冊。

根據上圖的描述,在本項目中可以在攝像頭初始化之後,緩存當前 3d 場景 eyes 眼睛的位置,以及 center 中心的位置,之後將 3d 場景 eyes 眼睛和 center 中心設置成攝像頭中心點的位置,然後在這個時刻獲取當前 3d 場景的截圖,該截圖即為當前攝像頭的監控圖像,之後再將 3d 場景的 center 與 eyes 設置成開始時緩存的 eyes 與 center 位置,通過該方法即可實現 3d 場景中任意位置的快照,從而實現攝像頭監控圖像實時生成。

相關偽代碼如下:

 1 function getFrontImg(camera, rangeNode) {
 2     var oldEye = g3d.getEye(); 3 var oldCenter = g3d.getCenter(); 4 var oldFovy = g3d.getFovy(); 5  g3d.setEye(攝像頭位置); 6  g3d.setCenter(攝像頭朝向); 7  g3d.setFovy(攝像頭張角); 8  g3d.setAspect(攝像頭寬高比); 9  g3d.validateImp(); 10  g3d.toDataURL(); 11  g3d.setEye(oldEye);; 12  g3d.setCenter(oldCenter); 13  g3d.setFovy(oldFovy); 14  g3d.setAspect(undefined); 15  g3d.validateImp(); 16 }

經過測試之後,通過該方法進行圖像的獲取會導致頁面有所卡頓,因為是獲取當前 3d 場景的整體截圖,由於當前3d場景是比較大的,所以 toDataURL 獲取圖像信息是非常慢的,因此我採取了離屏的方式來獲取圖像,具體方式如下:
   1. 創建一個新的 3d 場景,將當前場景的寬度與高度都設置為 200px 的大小,並且當前 3d 場景的內容與主屏的場景是一樣的,HT中通過 new ht.graph3d.Graph3dView(dataModel) 來新建場景,其中的 dataModel 為當前場景的所有圖元,所以主屏與離屏的 3d 場景都共用同一個 dataModel,保證了場景的一致。
   2. 將新創建的場景位置設置成屏幕看不到的地方,並且添加進 dom 中。
   3. 將之前對主屏獲取圖像的操作變成對離屏獲取圖像的操作,此時離屏圖像的大小相對之前主屏獲取圖像的大小小很多,並且離屏獲取不需要保存原來的眼睛 eyes 的位置以及 center 中心的位置,因為我們沒有改變主屏的 eyes 與 center 的位置, 所以也減少的切換帶來的開銷,大大提高了攝像頭獲取圖像的速度。

以下是該方法實現的代碼:

 1 function getFrontImg(camera, rangeNode) {
 2     // 截取當前圖像時將該攝像頭所屬的五面體隱藏
 3     rangeNode.s('shape3d.from.visible', false); 4 rangeNode.s('shape3d.visible', false); 5 rangeNode.s('wf.geometry', false); 6 var cameraP3 = camera.p3(); 7 var cameraR3 = camera.r3(); 8 var cameraS3 = camera.s3(); 9 var updateScreen = function() { 10  demoUtil.Canvas2dRender(camera, outScreenG3d.getCanvas()); 11  rangeNode.s({ 12 'shape3d.from.image': camera.a('canvas') 13  }); 14 rangeNode.s('shape3d.from.visible', true); 15 rangeNode.s('shape3d.visible', true); 16 rangeNode.s('wf.geometry', true); 17  }; 18 19 // 當前錐體起始位置 20 var realP3 = [cameraP3[0], cameraP3[1] + cameraS3[1] / 2, cameraP3[2] + cameraS3[2] / 2]; 21 // 將當前眼睛位置繞着攝像頭起始位置旋轉得到正確眼睛位置 22 var realEye = demoUtil.getCenter(cameraP3, realP3, cameraR3); 23 24  outScreenG3d.setEye(realEye); 25 outScreenG3d.setCenter(demoUtil.getCenter(realEye, [realEye[0], realEye[1], realEye[2] + 5], cameraR3)); 26 outScreenG3d.setFovy(camera.a('fovy')); 27  outScreenG3d.validate(); 28  updateScreen(); 29 }

 

上面代碼中有一個 getCenter 方法是用於獲取 3d 場景中點 A 繞着點 B 旋轉 angle 角度之後得到的點 A 在 3d 場景中的位置,方法中採用了 HT 封裝的 ht.Math 下面的方法,以下為代碼:

 1 // pointA 為 pointB 圍繞的旋轉點
 2 // pointB 為需要旋轉的點
 3 // r3 為旋轉的角度數組 [xAngle, yAngle, zAngle] 為繞着 x, y, z 軸分別旋轉的角度 
 4 var getCenter = function(pointA, pointB, r3) {
 5     var mtrx = new ht.Math.Matrix4(); 6 var euler = new ht.Math.Euler(); 7 var v1 = new ht.Math.Vector3(); 8 var v2 = new ht.Math.Vector3(); 9 10 mtrx.makeRotationFromEuler(euler.set(r3[0], r3[1], r3[2])); 11 12  v1.fromArray(pointB).sub(v2.fromArray(pointA)); 13  v2.copy(v1).applyMatrix4(mtrx); 14  v2.sub(v1); 15 16 return [pointB[0] + v2.x, pointB[1] + v2.y, pointB[2] + v2.z]; 17 };

這裏應用到向量的部分知識,具體如下:

OA + OB = OC

方法分為以下幾個步驟求解:

   1.  var mtrx = new ht.Math.Matrix4() 創建一個轉換矩陣,通過 mtrx.makeRotationFromEuler(euler.set(r3[0], r3[1], r3[2])) 獲取繞着 r3[0],r3[1],r3[2] 即 x 軸,y 軸,z 軸旋轉的旋轉矩陣。
   2. 通過 new ht.Math.Vector3() 創建 v1,v2 兩個向量。
   3. v1.fromArray(pointB) 為建立一個從原點到 pointB 的一個向量。
   4. v2.fromArray(pointA) 為建立一個從原點到 pointA 的一個向量。
   5. v1.fromArray(pointB).sub(v2.fromArray(pointA)) 即向量 OB – OA 此時得到的向量為 AB,此時 v1 變為向量 AB。
   6. v2.copy(v1) v2 向量拷貝 v1 向量,之後通過 v2.copy(v1).applyMatrix4(mtrx) 對 v2 向量應用旋轉矩陣,變換之後即為 v1向量繞着 pointA 旋轉之後的的向量 v2。
   7. 此時通過 v2.sub(v1) 就獲取了起始點為 pointB,終點為 pointB 旋轉之後點構成的向量,該向量此時即為 v2。
   8. 通過向量公式得到旋轉之後的點為 [pointB[0] + v2.x, pointB[1] + v2.y, pointB[2] + v2.z]。

項目中的 3D 場景例子其實是  最近貴州數博會,HT 上工業互聯網展台的 VR 示例,大眾對 VR/AR 的期待很高,但路還是得一步步走,即使融資了 23 億美金的 Magic Leap 的第一款產品也只能是 ,這話題以後再展開,這裏就上段當時現場的視頻照片:

2d 圖像貼到 3d 模型

通過上一步的介紹我們可以獲取當前攝像機位置的截屏圖像,那麼如何將當前圖像貼到前面所構建的五面體底部呢?前面通過 from_vs, from_is 來構建底部的長方形,所以在 HT 中可以通過將五面體的 style 中 shape3d.from.image 屬性設置成當前圖像,其中 from_uv 數組用來定義貼圖的位置,具體如下圖:

以下為定義貼圖位置 from_uv 的代碼:

 1 from_uv: [0, 0, 1, 0, 1, 1, 0, 1] 

from_uv 就是定義貼圖的位置數組,根據上圖的解釋,可以將 2d 圖像貼到 3d 模型的 from 面。

控制面板

HT 中通過 new ht.widget.Panel() 來生成如下圖的面板:

面板中每個攝像頭都有一個模塊來呈現當前監控圖像,其實這個地方也是一個 canvas,該 canvas 與場景中錐體前面的監控圖像是同一個 canvas,每一個攝像頭都有一個自己的 canvas 用來保存當前攝像頭的實時監控畫面,這樣就可以將該 canvas 貼到任何地方,將該 canvas 添加進面板的代碼如下:

 1 formPane.addRow([{ 2 element: camera.a(‘canvas’) 3 }], 240, 240); 

代碼中將 canvas 節點存儲在攝像頭圖元的 attr 屬性下面,之後便可以通過 camera.a(‘canvas’) 來獲取當前攝像頭的畫面。

在面板中的每一個控制節點都是通過 formPane.addRow 來進行添加,具體可參考 HT for Web 的。之後通過 ht.widget.Panel 將表單面板 formPane 添加進 panel 面板中,具體可參考 HT for Web 的。

部分控制代碼如下:

 1 formPane.addRow(['rotateY', {
 2  slider: { 3 min: -Math.PI, 4  max: Math.PI, 5 value: r3[1], 6 onValueChanged: function() { 7 var cameraR3 = camera.r3(); 8 camera.r3([cameraR3[0], this.getValue(), cameraR3[2]]); 9 rangeNode.r3([cameraR3[0], this.getValue(), cameraR3[2]]); 10  getFrontImg(camera, rangeNode); 11  } 12  } 13 }], [0.1, 0.15]);

控制面板通過 addRow 來添加控制元素,以上代碼為添加攝像頭繞着 y 軸進行旋轉的控制,onValueChanged 在 slider 的數值改變的時候調用,此時通過 camera.r3() 獲取當前攝像頭的旋轉參數, 由於是繞着 y 軸旋轉所以 x 軸與 z 軸的角度是不變的,變的是 y 軸的旋轉角度,所以通過 camera.r3([cameraR3[0], this.getValue(), cameraR3[2]]) 來調整攝像頭的旋轉角度以及通過 rangeNode.r3([cameraR3[0], this.getValue(), cameraR3[2]]) 來設置攝像頭前方錐體的旋轉角度,然後調用之前封裝好的 getFrontImg 函數來獲取此時旋轉角度下面的實時圖像信息。

項目中通過 Panel 面板的配置參數 titleBackground: rgba(230, 230, 230, 0.4) 即可將標題背景設置為具有透明度的背景,其它類似的 titleColor, titleHeight 等標題參數都可以配置,通過 separatorColor,separatorWidth 等分割參數可以設置內部面板之間分割線的顏色,寬度等。最後面板通過 panel.setPositionRelativeTo(‘rightTop’) 將面板的位置設置成右上角,並且通過 document.body.appendChild(panel.getView()) 將面板最外層的 div 添加進頁面中, panel.getView() 用來獲取面板的最外層 dom 節點。

具體初始化面板代碼如下:

 1 function initPanel() {
 2     var panel = new ht.widget.Panel(); 3 var config = { 4 title: "攝像頭控制面板", 5 titleBackground: 'rgba(230, 230, 230, 0.4)', 6 titleColor: 'rgb(0, 0, 0)', 7 titleHeight: 30, 8 separatorColor: 'rgb(67, 175, 241)', 9 separatorWidth: 1, 10 exclusive: true, 11  items: [] 12  }; 13 cameraArr.forEach(function(data, num) { 14 var camera = data['camera']; 15 var rangeNode = data['rangeNode']; 16 var formPane = new ht.widget.FormPane(); 17  initFormPane(formPane, camera, rangeNode); 18  config.items.push({ 19 title: "攝像頭" + (num + 1), 20 titleBackground: 'rgba(230, 230, 230, 0.4)', 21 titleColor: 'rgb(0, 0, 0)', 22 titleHeight: 30, 23 separatorColor: 'rgb(67, 175, 241)', 24 separatorWidth: 1, 25  content: formPane, 26 flowLayout: true, 27 contentHeight: 400, 28 width: 250, 29 expanded: num === 0 30  }); 31  }); 32  panel.setConfig(config); 33 panel.setPositionRelativeTo('rightTop'); 34  document.body.appendChild(panel.getView()); 35 window.addEventListener("resize", 36 function() { 37  panel.invalidate(); 38  }); 39 }

在控制面板中可以調整攝像頭的方向,攝像頭監控的輻射範圍,攝像頭前方錐體的長度等等,並且攝像頭的圖像是實時生成,以下為運行截圖:

以下是本項目採用的 3D 場景結合 HT for Web 的 VR 技術實現的操作:

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

如何回答性能優化的問題?

日常工作中,我們多少都會遇到應用的性能問題。在阿裏面試中,性能優化也是常被問到的題目,用來考察是否有實際的線上問題處理經驗。面對這類問題,阿里工程師齊光給出了詳細流程。來阿裏面試前,先看看這篇文章哦。

性能問題和Bug不同,後者的分析和解決思路更清晰,很多時候從應用日誌(文中的應用指分佈式服務下的單個節點)即可直接找到問題根源,而性能問題,其排查思路更為複雜一些。

對應用進行性能優化,是一個系統性的工程,對工程師的技術廣度和技術深度都有所要求。一個簡單的應用,它不僅包含了應用代碼本身,還和容器(虛擬機)、操作系統、存儲、網絡、文件系統等緊密相關,線上應用一旦出現了性能問題,需要我們從多方面去考慮。

與此同時,除了一些低級的代碼邏輯引發的性能問題外,很多性能問題隱藏的較深,排查起來會比較困難,需要我們對應用的各個子模塊、應用所使用的框架和組件的原理有所了解,同時掌握一定的性能優化工具和經驗。

本文總結了我們在進行性能優化時常用的一些工具及技巧,目的是希望通過一個全面的視角,去感知性能優化的整體脈絡。本文主要分為下面三個部分:

  1. 第一部分會介紹性能優化的一些背景知識。
  2. 第二部分會介紹性能優化的通用流程以及常見的一些誤區。
  3. 第三部分會從系統層和業務層的角度,介紹高效的性能問題定位工具和高頻性能瓶頸點分佈。

本文中提到的線程、堆、垃圾回收等名詞,如無特別說明,指的是 Java 應用中的相關概念。

 

1.性能優化的背景

前面提到過,應用出現性能問題和應用存在缺陷是不一樣的,後者大多數是由於代碼的質量問題導致,會導致應用功能性的缺失或出現風險,一經發現,會被及時修復。而性能問題,可能是由多方面的因素共同作用的結果:代碼質量一般、業務發展太快、應用架構設計不合理等,這些問題處理起來一般耗時較長、分析鏈路複雜,大家都不願意干,因此可能會被一些臨時性的補救手段所掩蓋,如:系統水位高或者單機的線程池隊列爆炸,那就集群擴容增加機器;內存佔用高/高峰時段 OOM,那就重啟分分鐘解決……

臨時性的補救措施只是在給應用埋雷,同時也只能解決部分問題。譬如,在很多場景下,加機器也並不能解決應用的性能問題,如對時延比較敏感的一些應用必須把單機的性能優化到極致,與此同時,加機器這種方式也造成了資源的浪費,長期來看是得不償失的。對應用進行合理的性能優化,可在應用穩定性、成本核算獲得很大的收益。

上面我們闡述了進行性能優化的必要性。假設現在我們的應用已經有了性能問題(eg. CPU 水位比較高),準備開始進行優化工作了,在這個過程中,潛在的痛點會有哪些呢?下面列出一些較為常見的:

  1. 對性能優化的流程不是很清晰。初步定為一個疑似瓶頸點后,就興高采烈地吭哧吭哧開始干,最終解決的問題其實只是一個淺層次的性能瓶頸,真實的問題的根源並未觸達;
  2. 對性能瓶頸點的分析思路不是很清晰。CPU、網絡、內存……這麼多的性能指標,我到底該關注什麼,應該從哪一塊兒開始入手?
  3. 對性能優化的工具不了解。遇到問題后,不清楚該用哪個工具,不知道通過工具得到的指標代表什麼。

    2.性能優化的流程

    在性能優化這個領域,並沒有一個嚴格的流程定義,但是對於絕大多數的優化場景,我們可以將其過程抽象為下面四個步驟。

    1. 準備階段:主要工作是是通過性能測試,了解應用的概況、瓶頸的大概方向,明確優化目標;
    2. 分析階段:通過各種工具或手段,初步定位性能瓶頸點;
    3. 調優階段:根據定位到的瓶頸點,進行應用性能調優;
    4. 測試階段:讓調優過的應用進行性能測試,與準備階段的各項指標進行對比,觀測其是否符合預期,如果瓶頸點沒有消除或者性能指標不符合預期,則重複步驟2和3。
    5. 下圖即為上述四個階段的簡要流程。

    2.1 通用流程詳解

    在上述通用流程的四個步驟當中,步驟2和3我們會在接下來兩個部分重點進行介紹。首先我們來看一下,在準備階段和測試階段,我們需要做一些什麼。

    | 2.1.1 準備階段

    準備階段是非常關鍵的一步,不能省略。

    首先,需要對我們進行調優的對象進行詳盡的了解,所謂知己知彼,百戰不殆。

    1. 對性能問題進行粗略評估,過濾一些因為低級的業務邏輯導致的性能問題。譬如,線上應用日誌級別不合理,可能會在大流量時導致 CPU 和磁盤的負載飆高,這種情況調整日誌級別即可;
    2. 了解應用的的總體架構,比如應用的外部依賴和核心接口有哪些,使用了哪些組件和框架,哪些接口、模塊的使用率較高,上下游的數據鏈路是怎麼樣的等;
    3. 了解應用對應的服務器信息,如服務器所在的集群信息、服務器的 CPU/內存信息、安裝的 Linux 版本信息、服務器是容器還是虛擬機、所在宿主機混部后是否對當前應用有干擾等;

    其次,我們需要獲取基準數據,然後結合基準數據和當前的一些業務指標,確定此次性能優化的最終目標。

    1. 使用基準測試工具獲取系統細粒度指標。可以使用若干 Linux 基準測試工具(eg. jmeter、ab、loadrunnerwrk、wrk等),得到文件系統、磁盤 I/O、網絡等的性能報告。除此之外,類似 GC、Web 服務器、網卡流量等信息,如有必要也是需要了解記錄的;
    2. 通過壓測工具或者壓測平台(如果有的話),對應用進行壓力測試,獲取當前應用的宏觀業務指標,譬如:響應時間、吞吐量、TPS、QPS、消費速率(對於有 MQ 的應用)等。壓力測試也可以省略,可以結合當前的實際業務和過往的監控數據,去統計當前的一些核心業務指標,如午高峰的服務 TPS。

    | 2.1.2 測試階段

    進入到這一階段,說明我們已經初步確定了應用性能瓶頸的所在,而且已經進行初步的調優了。檢測我們調優是否有效的方式,就是在仿真的條件下,對應用進行壓力測試。注意:由於 Java 有 JIT(just-in-time compilation)過程,因此壓力測試時可能需要進行前期預熱。

    如果壓力測試的結果符合了預期的調優目標,或者與基準數據相比,有很大的改善,則我們可以繼續通過工具定位下一個瓶頸點,否則,則需要暫時排除這個瓶頸點,繼續尋找下一個變量。

    2.2 注意事項

    在進行性能優化時,了解下面這些注意事項可以讓我們少走一些彎路。

    1. 性能瓶頸點通常呈現 2/8 分佈,即80%的性能問題通常是由20%的性能瓶頸點導致的,2/8 原則也意味着並不是所有的性能問題都值得去優化;
    2. 性能優化是一個漸進、迭代的過程,需要逐步、動態地進行。記錄基準后,每次改變一個變量,引入多個變量會給我們的觀測、優化過程造成干擾;
    3. 不要過度追求應用的單機性能,如果單機表現良好,則應該從系統架構的角度去思考; 不要過度追求單一維度上的極致優化,如過度追求 CPU 的性能而忽略了內存方面的瓶頸;
    4. 選擇合適的性能優化工具,可以使得性能優化取得事半功倍的效果;
    5. 整個應用的優化,應該與線上系統隔離,新的代碼上線應該有降級方案。

    3.瓶頸點分析工具箱

    性能優化其實就是找出應用存在性能瓶頸點,然後設法通過一些調優手段去緩解。性能瓶頸點的定位是較困難的,快速、直接地定位到瓶頸點,需要具備下面兩個條件:

    1. 恰到好處的工具;
    2. 一定的性能優化經驗。

    工欲善其事,必先利其器,我們該如何選擇合適的工具呢?不同的優化場景下,又該選擇那些工具呢?

    首選,我們來看一下大名鼎鼎的「性能工具(Linux Performance Tools-full)圖」,想必很多工程師都知道,它出自系統性能專家 Brendan Gregg。該圖從 Linux 內核的各個子系統出發,列出了我們在對各個子系統進行性能分析時,可使用的工具,涵蓋了監測、分析、調優等性能優化的方方面面。除了這張全景圖之外,Brendan Gregg 還單獨提供了基準測試工具(Linux Performance Benchmark Tools)圖、性能監測工具(Linux Performance Observability Tools)圖等,更詳細的內容請參考 Brendan Gregg 的網站說明。

    圖片來源:

    上面這張圖非常經典,是我們做性能優化時非常好的參考資料,但事實上,我們在實際運用的時候,會發現可能它並不是最合適的,原因主要有下面兩點:

    1)對分析經驗要求較高。上面這張圖其實是從 Linux 系統資源的角度去觀測性能指標的,這要求我們對 Linux 各個子系統的功能、原理要有所了解。舉例:遇到性能問題了,我們不會拿每個子系統下的工具都去試一遍,大多數情況是:我們懷疑某個子系統有問題,然後根據這張圖上列舉的工具,去觀測或者驗證我們的猜想,這無疑拔高了對性能優化經驗的要求;

    2)適用性和完整性不是很好。我們在分析性能問題時,從系統底層自底向上地分析是較低效的,大多數時候,從應用層面去分析會更加有效。性能工具(Linux Performance Tools-full)圖只是從系統層一個角度給出了工具集,如果從應用層開始分析,我們可以使用哪些工具?哪些點是我們首先需要關注的?

    鑒於上面若干痛點,下面給出了一張更為實用的「性能優化工具圖譜」,該圖分別從系統層、應用層(含組件層)的角度出發,列舉了我們在分析性能問題時首先需要關注的各項指標(其中?標註的是最需要關注的),這些點是最有可能出現性能瓶頸的地方。需要注意的是,一些低頻的指標或工具,在圖中並沒有列出來,如 CPU 中斷、索引節點使用、I/O事件跟蹤等,這些低頻點的排查思路較複雜,一般遇到的機會也不多,在這裏我們聚焦最常見的一些就可以了。

    對比上面的性能工具(Linux Performance Tools-full)圖,下圖的優勢在於:把具體的工具同性能指標結合了起來,同時從不同的層次去描述了性能瓶頸點的分佈,實用性和可操作性更強一些。系統層的工具分為CPU、內存、磁盤(含文件系統)、網絡四個部分,工具集同性能工具(Linux Performance Tools-full)圖中的工具基本一致。組件層和應用層中的工具構成為:JDK 提供的一些工具 + Trace 工具 + dump 分析工具 + Profiling 工具等。

    這裏就不具體介紹這些工具的具體用法了,我們可以使用 man 命令得到工具詳盡的使用說明,除此之外,還有另外一個查詢命令手冊的方法:info。info 可以理解為 man 的詳細版本,如果 man 的輸出不太好理解,可以去參考 info 文檔,命令太多,記不住也沒必要記住。

    上面這張圖該如何使用?

    首先,雖然從系統、組件、應用兩個三個角度去描述瓶頸點的分佈,但在實際運行時,這三者往往是相輔相成、相互影響的。系統是為應用提供了運行時環境,性能問題的本質就是系統資源達到了使用的上限,反映在應用層,就是應用/組件的各項指標開始下降;而應用/組件的不合理使用和設計,也會加速系統資源的耗盡。因此,分析瓶頸點時,需要我們結合從不同角度分析出的結果,抽出共性,得到最終的結論。

    其次,建議先從應用層入手,分析圖中標註的高頻指標,抓出最重要的、最可疑的、最有可能導致性能的點,得到初步的結論后,再去系統層進行驗證。這樣做的好處是:很多性能瓶頸點體現在系統層,會是多變量呈現的,譬如,應用層的垃圾回收(GC)指標出現了異常,通過 JDK 自帶的工具很容易觀測到,但是體現在系統層上,會發現系統當前的 CPU 利用率、內存指標都不太正常,這就給我們的分析思路帶來了困擾。

    最後,如果瓶頸點在應用層和系統層均呈現出多變量分佈,建議此時使用 ZProfiler、JProfiler 等工具對應用進行 Profiling,獲取應用的綜合性能信息(注:Profiling 指的是在應用運行時,通過事件(Event-based)、統計抽樣(Sampling Statistical)或植入附加指令(Byte-Code instrumentation)等方法,收集應用運行時的信息,來研究應用行為的動態分析方法)。譬如,可以對 CPU 進行抽樣統計,結合各種符號表信息,得到一段時間內應用內的代碼熱點。

    下面介紹在不同的分析層次,我們需要關注的核心性能指標,同時,也會介紹如何初步根據這些指標,判斷系統或應用是否存在性能瓶頸點,至於瓶頸點的確認、瓶頸點的成因、調優手段,將會在下一部分展開。

    3.1 CPU&&線程

    和 CPU 相關的指標主要有以下幾個。常用的工具有 top、 ps、uptime、 vmstat、 pidstat等。

    1. CPU利用率(CPU Utilization)
    2. CPU 平均負載(Load Average)
    3. 上下文切換次數(Context Switch)

    top – 12:20:57 up 25 days, 20:49, 2 users, load average: 0.93, 0.97, 0.79

    Tasks: 51 total, 1 running, 50 sleeping, 0 stopped, 0 zombie
    %Cpu(s): 1.6 us, 1.8 sy, 0.0 ni, 89.1 id, 0.1 wa, 0.0 hi, 0.1 si, 7.3 st
    KiB Mem : 8388608 total, 476436 free, 5903224 used, 2008948 buff/cache
    KiB Swap: 0 total, 0 free, 0 used. 0 avail Mem

    PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND

    119680 admin 20 0 600908 72332 5768 S 2.3 0.9 52:32.61 obproxy
    65877 root 20 0 93528 4936 2328 S 1.3 0.1 449:03.61 alisentry_cli

    第一行显示的內容:當前時間、系統運行時間以及正在登錄用戶數。load average 后的三個数字,依次表示過去 1 分鐘、5 分鐘、15 分鐘的平均負載(Load Average)。平均負載是指單位時間內,系統處於可運行狀態(正在使用 CPU 或者正在等待 CPU 的進程,R 狀態)和不可中斷狀態(D 狀態)的平均進程數,也就是平均活躍進程數,CPU 平均負載和 CPU 使用率並沒有直接關係。

    第三行的內容表示 CPU 利用率,每一列的含義可以使用 man 查看。CPU 使用率體現了單位時間內 CPU 使用情況的統計,以百分比的方式展示。計算方式為:CPU 利用率 = 1 – (CPU 空閑時間)/ CPU 總的時間。需要注意的是,通過性能分析工具得到的 CPU 的利用率其實是某個採樣時間內的 CPU 平均值。注:top 工具显示的的 CPU 利用率是把所有 CPU 核的數值加起來的,即 8 核 CPU 的利用率最大可以到達800%(可以用 htop 等更新一些的工具代替 top)。

    使用 vmstat 命令,可以查看到「上下文切換次數」這個指標,如下錶所示,每隔1秒輸出1組數據:

    $ vmstat 1

    procs ———–memory———- —swap– —–io—- -system– ——cpu—–
    r b swpd free buff cache si so bi bo in cs us sy id wa st
    0 0 0 504804 0 1967508 0 0 644 33377 0 1 2 2 88 0 9

    上表的 cs(context switch) 就是每秒上下文切換的次數,按照不同場景,CPU 上下文切換還可以分為中斷上下文切換、線程上下文切換和進程上下文切換三種,但是無論是哪一種,過多的上下文切換,都會把 CPU 時間消耗在寄存器、內核棧以及虛擬內存等數據的保存和恢復上,從而縮短進程真正運行的時間,導致系統的整體性能大幅下降。vmstat 的輸出中 us、sy 分別用戶態和內核態的 CPU 利用率,這兩個值也非常具有參考意義。

    vmstat 的輸只給出了系統總體的上下文切換情況,要想查看每個進程的上下文切換詳情(如自願和非自願切換),需要使用 pidstat,該命令還可以查看某個進程用戶態和內核態的 CPU 利用率。

    CPU 相關指標異常的分析思路是什麼?

    1)CPU 利用率:如果我們觀察某段時間系統或應用進程的 CPU利用率一直很高(單個 core 超過80%),那麼就值得我們警惕了。我們可以多次使用 jstack 命令 dump 應用線程棧查看熱點代碼,非 Java 應用可以直接使用 perf 進行 CPU 采採樣,離線分析採樣數據后得到 CPU 執行熱點(Java 應用需要符號表進行堆棧信息映射,不能直接使用 perf得到結果)。

    2)CPU 平均負載:平均負載高於 CPU 數量 70%,意味着系統存在瓶頸點,造成負載升高的原因有很多,在這裏就不展開了。需要注意的是,通過監控系統監測平均負載的變化趨勢,更容易定位問題,有時候大文件的加載等,也會導致平均負載瞬時升高。如果 1 分鐘/5 分鐘/15 分鐘的三個值相差不大,那說明系統負載很平穩,則不用關注,如果這三個值逐漸降低,說明負載在漸漸升高,需要關注整體性能;

    3)CPU 上下文切換:上下文切換這個指標,並沒有經驗值可推薦(幾十到幾萬都有可能),這個指標值取決於系統本身的 CPU 性能,以及當前應用工作的情況。但是,如果系統或者應用的上下文切換次數出現數量級的增長,就有很大概率說明存在性能問題,如非自願上下切換大幅度上升,說明有太多的線程在競爭 CPU。

    上面這三個指標是密切相關的,如頻繁的 CPU 上下文切換,可能會導致平均負載升高。如何根據這三者之間的關係進行應用調優,將在下一部分介紹。

    CPU 上的的一些異動,通常也可以從線程上觀測到,但需要注意的是,線程問題並不完全和 CPU 相關。與線程相關的指標,主要有下面幾個(均都可以通過 JDK 自帶的 jstack 工具直接或間接得到):

    1. 應用中的總的線程數;
    2. 應用中各個線程狀態的分佈;
    3. 線程鎖的使用情況,如死鎖、鎖分佈等;

    關於線程,可關注的異常有:

    1)線程總數是否過多。過多的線程,體現在 CPU 上就是導致頻繁的上下文切換,同時線程過多也會消耗內存,線程總數大小和應用本身和機器配置相關;

    2)線程的狀態是否異常。觀察 WAITING/BLOCKED 線程是否過多(線程數設置過多或鎖競爭劇烈),結合應用內部鎖使用的情況綜合分析;

    3)結合 CPU 利用率,觀察是否存在大量消耗 CPU 的線程。

    3.2 內存&&堆

    和內存相關的指標主要有以下幾個,常用的分析工具有:top、free、vmstat、pidstat 以及 JDK 自帶的一些工具。

    1. 系統內存的使用情況,包括剩餘內存、已用內存、可用內存、緩存/緩衝區;
    2. 進程(含 Java 進程)的虛擬內存、常駐內存、共享內存;
    3. 進程的缺頁異常數,包含主缺頁異常和次缺頁異常;
    4. Swap 換入和換出的內存大小、Swap 參數配置;
    5. JVM 堆的分配,JVM 啟動參數;
    6. JVM 堆的回收,GC 情況。

    使用 free 可以查看系統內存的使用情況和 Swap 分區的使用情況,top 工具可以具體到每個進程,如我們可以用使用 top 工具查看 Java 進程的常駐內存大小(RES),這兩個工具結合起來,可用覆蓋大多數內存指標。下面是使用 free命令的輸出:

    $free -h

              total        used        free shared buff/cache available

    Mem: 125G 6.8G 54G 2.5M 64G 118G

    Swap: 2.0G 305M 1.7G

    上述輸出各列的具體含義在這裏不在贅述,也比較容易理解。重點介紹下 swap 和 buff/cache 這兩個指標。

    Swap 的作用是把一個本地文件或者一塊磁盤空間作為內存來使用,包括換出和換入兩個過程。Swap 需要讀寫磁盤,所以性能不是很高,事實上,包括 ElasticSearch 、Hadoop 在內絕大部分 Java 應用都建議關掉 Swap,這是因為內存的成本一直在降低,同時這也和 JVM 的垃圾回收過程有關:JVM在 GC 的時候會遍歷所有用到的堆的內存,如果這部分內存被 Swap 出去了,遍歷的時候就會有磁盤 I/O 產生。Swap 分區的升高一般和磁盤的使用強相關,具體分析時,需要結合緩存使用情況、swappiness 閾值以及匿名頁和文件頁的活躍情況綜合分析。

    buff/cache 是緩存和緩衝區的大小。緩存(cache):是從磁盤讀取的文件的或者向磁盤寫文件時的臨時存儲數據,面向文件。使用 cachestat 可以查看整個系統緩存的讀寫命中情況,使用 cachetop 可以觀察每個進程緩存的讀寫命中情況。緩衝區(buffer)是寫入磁盤數據或從磁盤直接讀取的數據的臨時存儲,面向塊設備。free 命令的輸出中,這兩個指標是加在一起的,使用 vmstat 命令可以區分緩存和緩衝區,還可以看到 Swap 分區換入和換出的內存大小。

    了解到常見的內存指標后,常見的內存問題又有哪些?總結如下:

    1. 系統剩餘內存/可用不足(某個進程佔用太多、系統本身內存不足),內存溢出;
    2. 內存回收異常:內存泄漏(進程在一段時間內內存使用持續走高)、GC 頻率異常;
    3. 緩存使用過大(大文件讀取或寫入)、緩存命中率不高;
    4. 缺頁異常過多(頻繁的 I/O 讀);
    5. Swap 分區使用異常(使用過大);

    內存相關指標異常后,分析思路是怎麼樣的?

    1. 使用 free/top 查看內存的全局使用情況,如系統內存的使用、Swap 分區內存使用、緩存/緩衝區佔用情況等,初步判斷內存問題存在的方向:進程內存、緩存/緩衝區、Swap 分區;
    2. 觀察一段時間內存的使用趨勢。如通過 vmstat 觀察內存使用是否一直在增長;通過 jmap 定時統計對象內存分佈情況,判斷是否存在內存泄漏,通過 cachetop 命令,定位緩衝區升高的根源等;
    3. 根據內存問題的類型,結合應用本身,進行詳細分析。

    舉例:使用 free 發現緩存/緩衝區佔用不大,排除緩存/緩衝區對內存的影響后 -> 使用 vmstat 或者 sar 觀察一下各個進程內存使用變化趨勢 -> 發現某個進程的內存時候用持續走高 -> 如果是 Java 應用,可以使用 jmap / VisualVM / heap dump 分析等工具觀察對象內存的分配,或者通過 jstat 觀察 GC 后的應用內存變化 -> 結合業務場景,定位為內存泄漏/GC參數配置不合理/業務代碼異常等。

    3.3 磁盤&&文件

    在分析和磁盤相關的問題時,通常是將其和文件系統同時考慮的,下面不再區分。和磁盤/文件系統相關的指標主要有以下幾個,常用的觀測工具為 iostat和 pidstat,前者適用於整個系統,後者可觀察具體進程的 I/O。

    1. 磁盤 I/O 利用率:是指磁盤處理 I/O 的時間百分比;
    2. 磁盤吞吐量:是指每秒的 I/O 請求大小,單位為 KB;
    3. I/O 響應時間,是指 I/O 請求從發出到收到響應的間隔,包含在隊列中的等待時間和實際處理時間;
    4. IOPS(Input/Output Per Second):每秒的 I/O 請求數;
    5. I/O 等待隊列大小,指的是平均 I/O 隊列長度,隊列長度越短越好;

    使用 iostat 的輸出界面如下:

    $iostat -dx

    Linux 3.10.0-327.ali2010.alios7.x86_64 (loginhost2.alipay.em14) 10/20/2019 x86_64 (32 CPU)

    Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
    sda 0.01 15.49 0.05 8.21 3.10 240.49 58.92 0.04 4.38 2.39 4.39 0.09 0.07

    上圖中 %util ,即為磁盤 I/O 利用率,同 CPU 利用率一樣,這個值也可能超過 100%(存在并行 I/O);rkB/s 和 wkB/s分別表示每秒從磁盤讀取和寫入的數據量,即吞吐量,單位為 KB;磁盤 I/O處理時間的指標為 r_await 和 w_await 分別表示讀/寫請求處理完成的響應時間,svctm 表示處理 I/O 所需要的平均時間,該指標已被廢棄,無實際意義。r/s + w/s 為 IOPS 指標,分別表示每秒發送給磁盤的讀請求數和寫請求數;aqu-sz 表示等待隊列的長度。

    pidstat 的輸出大部分和 iostat 類似,區別在於它可以實時查看每個進程的 I/O 情況。

    如何判斷磁盤的指標出現了異常?

      1. 當磁盤 I/O 利用率長時間超過 80%,或者響應時間過大(對於 SSD,從 0.0x 毫秒到 1.x 毫秒不等,机械磁盤一般為5ms~10ms),通常意味着磁盤 I/O 存在性能瓶頸;
      2. 如果 %util 很大,而 rkB/s 和 wkB/s 很小,一般是因為存在較多的磁盤隨機讀寫,最好把隨機讀寫優化成順序讀寫,(可以通過 strace 或者 blktrace 觀察 I/O 是否連續判斷是否是順序的讀寫行為,隨機讀寫應可關注 IOPS 指標,順序讀寫可關注吞吐量指標);
      3. 如果 avgqu-sz 比較大,說明有很多 I/O 請求在隊列中等待。一般來說,如果單塊磁盤的隊列長度持續超過2,一般認為該磁盤存在 I/O 性能問題。

    3.4 網絡

    網絡這個概念涵蓋的範圍較廣,在應用層、傳輸層、網絡層、網絡接口層都有不同的指標去衡量。這裏我們討論的「網絡」,特指應用層的網絡,通常使用的指標如下:

    1. 網絡帶寬:表示鏈路的最大傳輸速率;
    2. 網絡吞吐:表示單位時間內成功傳輸的數據量大小;
    3. 網絡延時:表示從網絡請求發出后直到收到遠端響應,所需要的時間;
    4. 網絡連接數和錯誤數;

    一般來說,應用層的網絡瓶頸有如下幾類:

    1. 集群或機器所在的機房的網絡帶寬飽和,影響應用 QPS/TPS 的提升;
    2. 網絡吞吐出現異常,如接口存在大量的數據傳輸,造成帶寬佔用過高;
    3. 網絡連接出現異常或錯誤;
    4. 網絡出現分區。

    帶寬和網絡吞吐這兩個指標,一般我們會關注整個應用的,通過監控系統可直接得到,如果一段時間內出現了明顯的指標上升,說明存在網絡性能瓶頸。對於單機,可以使用 sar 得到網絡接口、進程的網絡吞吐。

    使用 ping 或者 hping3 可以得到是否出現網絡分區、網絡具體時延。對於應用,我們更關注整個鏈路的時延,可以通過中間件埋點后輸出的 trace 日誌得到鏈路上各個環節的時延信息。

    使用 netstat、ss 和 sar 可以獲取網絡連接數或網絡錯誤數。過多網絡鏈接造成的開銷是很大的,一是會佔用文件描述符,二是會佔用緩存,因此系統可以支撐的網絡鏈接數是有限的。

    3.5 工具總結

    可以看到的是,在分析 CPU、內存、磁盤等的性能指標時,有幾種工具是高頻出現的,如 top、vmstat、pidstat,這裏稍微總結一下:

    1. CPU:top、vmstat、pidstat、sar、perf、jstack、jstat;
    2. 內存:top、free、vmstat、cachetop、cachestat、sar、jmap;
    3. 磁盤:top、iostat、vmstat、pidstat、du/df;
    4. 網絡:netstat、sar、dstat、tcpdump;
    5. 應用:profiler、dump分析。

    上述的很多工具,大部分是用於查看系統層指標的,在應用層,除了有 JDK 提供的一系列工具,一些商用的產品如 gceasy.io(分析 GC 日誌)、fastthread.io(分析線程 dump 日誌)也是不錯的。

    排查 Java 應用的線上異常或者分析應用代碼瓶頸,可以使用阿里開源的 Arthas ,這個工具非常強大,下面簡單介紹下。

    Arthas 主要面向線上應用實時診斷,解決的是類似「線上應用異常了,需要在線進行分析和定位」的問題,當然,Arthas 提供的一些方法調用追蹤工具,對我們排查諸如「慢查詢」等問題,也是非常有幫助的。Arthas 提供的主要功能有:

    1. 獲取線程統計,如線程持有的鎖統計、CPU 利用率統計等;
    2. 類加載信息、動態類加載、方法加載信息;
    3. 調用棧追蹤,調用耗時統計;
    4. 方法調用參數、結果檢測;
    5. 系統配置、應用配置信息;
    6. 反編譯加載類;
    7. ….

    需要注意的是,性能工具只是解決性能問題的手段,我們了解常用工具的一般用法即可,不要在工具學習上投入過多精力。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

Python 併發總結,多線程,多進程,異步IO

1 測量函數運行時間

import time
def profile(func):
    def wrapper(*args, **kwargs):
        import time
        start = time.time()
        func(*args, **kwargs)
        end   = time.time()
        print 'COST: {}'.format(end - start)
    return wrapper
 
@profile
def fib(n):
    if n<= 2:
        return 1
    return fib(n-1) + fib(n-2)
 
fib(35)
 

 

2 啟動多個線程,並等待完成   2.1 使用threading.enumerate()

import threading
for i in range(2):
    t = threading.Thread(target=fib, args=(35,))
    t.start()
main_thread = threading.currentThread()
 
for t in threading.enumerate():
    if t is main_thread:
        continue
    t.join()

 

2.2 先保存啟動的線程

threads = []
for i in range(5):
    t = Thread(target=foo, args=(i,))
    threads.append(t)
    t.start()
for t in threads:
    t.join()

  3 使用信號量,限制同時能有幾個線程訪問臨界區

from threading import Semaphore
import time
 
sema = Semaphore(3)
 
def foo(tid):
    with sema:
        print('{} acquire sema'.format(tid))
        wt = random() * 2
        time.sleep(wt)
        print('{} release sema'.format(tid))
 

 

4 鎖,相當於信號量為1的情況

from threading import Thread Lock
value = 0
lock = Lock()
def getlock():
    global lock
    with lock:
        new = value + 1
        time.sleep(0.001)
        value = new

 

  5 可重入鎖RLock     acquire() 可以不被阻塞的被同一個線程調用多次,release()需要和acquire()調用次數匹配才能釋放鎖 6 條件 Condition 一個線程發出信號,另一個線程等待信號 常用於生產者-消費者模型

import time
import threading
 
def consumer(cond):
    t = threading.currentThread()
    with cond:
        cond.wait()
        print("{}: Resource is available to sonsumer".format(t.name))
 
def producer(cond):
    t = threading.currentThread()
    with cond:
        print("{}: Making resource available".format(t.name))
        cond.notifyAll()
 
condition = threading.Condition()
c1 = threading.Thread(name='c1', target=consumer, args=(condition,))
c2 = threading.Thread(name='c2', target=consumer, args=(condition,))
p = threading.Thread(name='p', target=producer, args=(condition,))
 
c1.start()
c2.start()
p.start()

 

  7 事件 Event 感覺和Condition 差不多

import time
import threading
from random import randint
 
TIMEOUT = 2
 
def consumer(event, l):
    t = threading.currentThread()
    while 1:
        event_is_set = event.wait(TIMEOUT)
        if event_is_set:
            try:
                integer = l.pop()
                print '{} popped from list by {}'.format(integer, t.name)
                event.clear()  # 重置事件狀態
            except IndexError:  # 為了讓剛啟動時容錯
                pass
 
def producer(event, l):
    t = threading.currentThread()
    while 1:
        integer = randint(10, 100)
        l.append(integer)
        print '{} appended to list by {}'.format(integer, t.name)
        event.set()  # 設置事件
        time.sleep(1)
 
event = threading.Event()
l = []
 
threads = []
 
for name in ('consumer1', 'consumer2'):
    t = threading.Thread(name=name, target=consumer, args=(event, l))
    t.start()
    threads.append(t)
 
p = threading.Thread(name='producer1', target=producer, args=(event, l))
p.start()
threads.append(p)
 
 
for t in threads:
    t.join()

 

  8 線程隊列  線程隊列有task_done() 和 join() 標準庫里的例子 往隊列內放結束標誌,注意do_work阻塞可能無法結束,需要用超時

import queue
def worker():
    while True:
        item = q.get()
        if item is None:
            break
        do_work(item)
        q.task_done()
q = queue.Queue()
threads = []
for i in range(num_worker_threads):
    t = threading.Thread(target=worker)
    t.start()
    threads.append(t)
for item in source():
    q.put(item)
q.join()
for i in range(num_worker_threads):
    q.put(None)
for t in threads:
    t.join()

 

  9 優先級隊列 PriorityQueue

import threading
from random import randint
from queue import PriorityQueue
 
q = PriorityQueue()
 
def double(n):
    return n * 2
 
def producer():
    count = 0
    while 1:
        if count > 5:
            break
        pri = randint(0, 100)
        print('put :{}'.format(pri))
        q.put((pri, double, pri))  # (priority, func, args)
        count += 1
 
def consumer():
    while 1:
        if q.empty():
            break
        pri, task, arg = q.get()
        print('[PRI:{}] {} * 2 = {}'.format(pri, arg, task(arg)))
        q.task_done()
        time.sleep(0.1)
 
t = threading.Thread(target=producer)
t.start()
time.sleep(1)
t = threading.Thread(target=consumer)
t.start()

 

  10 線程池 當線程執行相同的任務時用線程池 10.1 multiprocessing.pool 中的線程池

from multiprocessing.pool import ThreadPool
pool = ThreadPool(5)
pool.map(lambda x: x**2, range(5))

 

10.2 multiprocessing.dummy

from multiprocessing.dummy import Pool

 

10.3 concurrent.futures.ThreadPoolExecutor

from concurrent.futures improt ThreadPoolExecutor
from concurrent.futures import as_completed
import urllib.request
 
URLS = ['http://www.baidu.com', 'http://www.hao123.com']
 
def load_url(url, timeout):
    with urllib.request.urlopen(url, timeout=timeout) as conn:
        return conn.read()
 
with ThreadPoolExecutor(max_workers=5) as executor:
    future_to_url = {executor.submit(load_url, url, 60): url for url in URLS}
    for future in as_completed(future_to_url):
        url = future_to_url[future]
        try:
            data = future.result()
        execpt Exception as exc:
            print("%r generated an exception: %s" % (url, exc))
        else:
            print("%r page is %d bytes" % (url, len(data)))
 

 

11 啟動多進程,等待多個進程結束

import multiprocessing
jobs = []
for i in range(2):
    p = multiprocessing.Process(target=fib, args=(12,))
    p.start()
    jobs.append(p)
for p in jobs:
    p.join()
 

 

12 進程池 12.1 multiprocessing.Pool

from multiprocessing import Pool
pool = Pool(2)
pool.map(fib, [36] * 2)

 

  12.2 concurrent.futures.ProcessPoolExecutor

from concurrent.futures import ProcessPoolExecutor
import math
 
PRIMES = [ 112272535095293, 112582705942171]
 
def is_prime(n):
    if n < 2:
        return False
    if n == 2:
        return True
    if n % 2 == 0:
        return False
    sqrt_n = int(math.floor(math.sqrt(n)))
    for i in range(3, sqrt_n + 1, 2):
        if n % i == 0:
            return False
    return True
 
if __name__ == "__main__":
    with ProcessPoolExecutor() as executor:
        for number, prime in zip(PRIMES, executor.map(is_prime, PRIMES)):
            print("%d is prime: %s" % (number, prime))
 

 

  13 asyncio   13.1 最基本的示例,單個任務

import asyncio
 
async def hello():
    print("Hello world!")
    await asyncio.sleep(1)
    print("Hello again")
 
loop = asyncio.get_event_loop()
loop.run_until_complete(hello())
loop.close()

 

13.2 最基本的示例,多個任務

import asyncio
 
async def hello():
    print("Hello world!")
    await asyncio.sleep(1)
    print("Hello again")
 
loop = asyncio.get_event_loop()
tasks = [hello(), hello()]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

 

  13.3 結合httpx 執行多個任務並接收返回結果 httpx 接口和 requests基本一致

import asyncio
import httpx
 
 
async def get_url():
    r = await httpx.get("http://www.baidu.com")
    return r.status_code
 
 
loop = asyncio.get_event_loop()
tasks = [get_url() for i in range(10)]
results = loop.run_until_complete(asyncio.gather(*tasks))
loop.close()
 
 
for num, result in zip(range(10), results):
    print(num, result)
 

 

   本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

Java基礎(六) static五大應用場景

static和final是兩個我們必須掌握的關鍵字。不同於其他關鍵字,他們都有多種用法,而且在一定環境下使用,可以提高程序的運行性能,優化程序的結構。上一個章節我們講了final關鍵字的原理及用法,本章節我們來了解一下static關鍵字原理及其用法。

一. static特點

static是一個修飾符,通常用於修飾變量和方法,如開發過程中用到的字典類數據都會用到static來修飾,工具類方法,如Dateutils,Stringutils這類工具方法也會用到static來修飾,那麼除了這兩種最常用的場景外,是否還有其他場景呢,答案是:有的,總共五種:

  1. static變量
  2. static方法
  3. static代碼塊
  4. static內部類
  5. static包內導入

static修飾的變量、方法、代碼塊、內部類在類加載期間就已經完成初始化,存儲在Java Heap(JDK7.0之前存儲在方法區)中靜態存儲區,因此static優於對象而存在。

static修飾的成員(變量、方法)被所有對象所共享,也叫靜態變量或靜態方法,可直接通過類調用(也建議通過類調用)。

二. static 變量

static變量隨着類的加載而存在,隨着類的消失而消失,當類被加載時,就會為靜態變量在Java Heap中分配內存空間,可以通過【類.變量名】和【對象.變量名】的方式調用,建議直接使用【類.變量名】的方式,

public class Person {
    private String name;

    private static int eyeNum;

    public static int legNum = 2;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public static int getEyeNum() {
        return eyeNum;
    }

    public static void setEyeNum(int eyeNum) {
        Person.eyeNum = eyeNum;
    }
}
public static void main(String[] args) {
    Person person = new Person();
    person.setEyeNum(25);

    Person person1 = new Person();
    person1.setEyeNum(28);
    System.out.println(person.getEyeNum());//28
    System.out.println(person1.getEyeNum());//28
    
    int legNum = person.legNum;
    System.out.println(legNum);//2
}

從上面的例子可以看出靜態變量是對所有對象共享,一個對象對其值的改動,直接就會造成另一個對象取值的不同。

什麼時候使用static變量?

作為共享變量使用,通常搭配final關鍵字一起使用,比如我們常用的字典類數據;

private static final String GENERAL_MAN = "man";

減少對象的創建,比如在類開頭的部分,定義Logger方法,用於異常日誌採集

private static Logger LOGGER = LogFactory.getLoggger(MyClass.class);

始終返回同一變量,比如我們的單例模式。

三. static 方法

靜態方法只能訪問靜態成員(靜態變量、靜態方法),而非靜態方法既可訪問靜態方法也可訪問非靜態方法;因為靜態成員優於對象而存在,因此無法調用和對象相關的關鍵字,如this,super,無法通過關鍵字訪問對象資源。

public class Person {
    private String name;    
    private static int eyeNum;    
    public static int legNum = 2;    
    public String getName() {
        return name;    
    }    
    public void setName(String name) {
        this.name = name;    
    }    
    public static int getEyeNum() {
        System.out.println(name);//編譯出錯,name不可用
        return eyeNum;
    }    
    public static void setEyeNum(int eyeNum) {
        Person.eyeNum = eyeNum;        
        this.name = "";//編譯出錯,this不可用
    }
}

什麼時候使用static方法?

static方法一般用於與當前對象無法的工廠方法、工具方法。如Math.sqrt(),Arrays.sort(),StringUtils.isEmpty()等。

四. static 代碼塊

static代碼塊相對於static變量和static方法來說使用不是那麼廣泛,但也算是比較常見的用法了,static代碼塊在加載一個類的時候最先執行,且只執行一次。

public static Map<String, String> timeTypes;
static {
    timeTypes = new HashMap<>();
    timeTypes.put("year", "年");
    timeTypes.put("quarter", "季");
    timeTypes.put("month", "月");
    timeTypes.put("day", "日");
    System.out.println("初始化1");
}
public static void main(String[] args) {
    System.out.println("初始化2");
}

執行結果是:

初始化1;

初始化2;

什麼時候使用static代碼塊?

一般在進行初始化操作時,比如讀取配置文件信息,獲取當前服務器參數等

五. static內部類

定義一個內部類,加上static,就成為了一個static內部類,static只能修飾內部類,不能修飾頂級類,靜態內部類在業務應用系統開發中使用的不多。

public class StaticCouter {
    private String str0 = "hi";    //非靜態變量    
    private static String str1 = "hello";  //靜態變量   
    static class StaticInnerClass{//靜態內部類
        public void getMessage(){
            System.out.println(str0);//編譯出錯
            System.out.println(str1);
        }
    }    
    class NormalInnerClass{//非靜態內部類
        public void getMessage(){
            System.out.println(str0);
            System.out.println(str1);
        }
    }
}

靜態內部類與非靜態內部類有何異同?

靜態內部類 非靜態內部類
不需要有指向外部類的引用 必須通過外部類的new關鍵字引用
可定義普通變量和方法,也可定義靜態變量和方法 可定義普通變量和方法,不可定義靜態變量和方法
可以調用外部類的靜態成員,不能調用外部類的普通成員 可調用外部類的普通成員和靜態成員
public static void main(String[] args) {
    //創建靜態內部類實例    
    StaticInnerClass staticInnerClass = new StaticInnerClass();    
    //調用靜態內部類方法    
    staticInnerClass.getMessage();    
    //創建靜態內部類實例    
    StaticCouter.StaticInnerClass staticInnerClass1 = new staticCouter.StaticInnerClass();    
    //調用靜態內部類方法
    staticInnerClass1.getMessage();
    //創建普通內部類實例
    StaticCouter.NormalInnerClass normalInnerClass = new StaticCouter().new NormalInnerClass();
    //調用普通內部類方法
    normalInnerClass.getMessage();
}

六. static包內導入

這個概念不太好理解,舉個例子

public static void main(String[] args) {
    int[] arra = {1,4,5,7};
    Arrays.sort(arra);
    Arrays.asList(arra);
    Arrays.fill(arra, 6);
}

static包導入目的就是去掉重複的Arrays類名調用

通過在頂部引入

import static java.util.Arrays.*

即可把Arrays類中所有的靜態變量,方法,內部類等都引入當前類中,調用時直接調用sort(arra),asList(arra),

java5后引入的,不常用,調用類方法時會比較簡單,但可讀性不好,慎用。

七. 總結

static是java中很常用的一個關鍵字,使用場景也很多,本文主要介紹了它的五種用法,static變量,static方法,static代碼塊,static內部類,static包內導入,若有不對之處,請批評指正,望共同進步,謝謝!

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

kubernetes高級之創建只讀文件系統以及只讀asp.net core容器

系列目錄

使用docker創建只讀文件系統

容器化部署對應用的運維帶來了極大的方便,同時也帶來一些新的安全問題需要考慮.比如黑客入侵到容器內,對容器內的系統級別或者應用級別文件進行修改,會造成難以估量的損失.(比如修改hosts文件導致dns解析異常,修改web資源導致網站被嵌入廣告,後端邏輯被更改導致權限驗證失效等,由於是分佈式部署,哪些容器內的資源被修改也很難以發現).解決這個問題的辦法就是創建創建一個具有隻讀文件系統的容器.下面介紹使用docker run命令和docker compose來創建具有隻讀文件系統的容器.

使用docker run命令創建只讀文件系統

比如說要創建一個只讀文件系統的redis容器,可以執行以下命令

docker run --read-only redis

docker compose/swarm創建只讀文件系統

yaml編排文件示例如下

version: '3.3'
 
services:
  redis:
    image: redis:4.0.1-alpine
    networks:
      - myoverlay
    read_only: true

networks:
  myoverlay:

問題:創建只讀文件系統看起來很不錯,但是實際上往往會有各種各樣的問題,比如很多應用要寫temp文件或者寫日誌文件,如果對這樣的應用創建只讀容器則很可能導致應用無法正常啟動.對於需要往固定位置寫入日誌或者臨時文件的應用,可以掛載宿主機的存儲卷,雖然容器是只讀的,但是掛載的盤仍然是可讀寫的.

創建只讀的asp.net core容器

上面一節我們講到了創建容器只讀文件系統以增加安全性,以及如何解決需要寫入日誌文件或者臨時文件這樣常見的問題.我們嘗試創建一個只讀的asp.net應用時,即便不使用任何log組件(即不寫入日誌),仍然無法正常啟動鏡像.解決這個問題其實也非常簡單,只需要把環境變量COMPlus_EnableDiagnostics的值設置為0即可.

FROM microsoft/dotnet:2.2-aspnetcore-runtime AS base
WORKDIR /app
EXPOSE 52193

FROM microsoft/dotnet:2.2-sdk AS build
WORKDIR /src
COPY . .
WORKDIR "/src"
RUN dotnet build "ReadOnlyTest.csproj" -c Release -o /app

FROM build AS publish
RUN dotnet publish "ReadOnlyTest.csproj" -c Release -o /app

FROM base AS final
WORKDIR /app
ENV DOTNET_RUNNING_IN_CONTAINER=true
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1

ENV COMPlus_EnableDiagnostics=0

COPY --from=publish /app .
ENTRYPOINT ["dotnet", "ReadOnlyTest.dll"]

我們對這些環境變量進行簡單介紹

  • DOTNET_RUNNING_IN_CONTAINER值設置為true時則表示應用運行在容器內,方便我們獲取程序的運行環境,然後根據環境做出不同決策(比如單元測試的時候,可能要根據項目是運行在windows,linux或者linux容器做出不同的測試策略).當然,你也可以設置其它的環境變量來方便自己使用,比如你鍵名稱設置為IsRunningInDocker,但是DOTNET_RUNNING_IN_CONTAINER

  • DOTNET_CLI_TELEMETRY_OPTOUT是否輸出遙測信息,如果設置為1則是關閉,這樣dotnet.exe就不會向調試窗口輸出遙測信息.

  • COMPlus_EnableDiagnostics目前沒有找到太多關於這個參數的詳細信息,只是查閱資源發現這開啟這項配置可以創建只讀權限 aspnet 應用程序.

微軟官方基礎鏡像里還包含一項名稱叫作ASPNETCORE_VERSION的環境變量,我們可以直接讀取它,這樣使用公共的環境變量一來避免息手動設置和更新的麻煩,二來便於和社區交流(自己定義約束的只能用於內部團隊交流)

我們如何使用這些環境變量呢,其它可以在程序裏面暴露一個helper方法,比如

private bool InDocker { get { return Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true";} }

這樣我們就可以根據實際的需求來使用它.

上面我們介紹了如何使用docker run命令以及docker-compose創建只讀文件系統.然而在kubernetes集群里,我們需要使用k8s的編排方法來創建只讀文件系統.那麼在k8s里如何創建只讀文件系統.其實這裏涉及到了另一個高級主題:那就是k8s的安全策略(Pod Security Policies)我們將在下一節介紹它.

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

Linux下Jenkins與GitHub自動構建NetCore與部署

今天我們來談談NetCore在Linux底下的持續集成與部署。NetCore我就不多介紹了,持續集成用的是Jenkins,源代碼管理器用的是GitHub。我們就跟着博文往下走吧。

        • 1.Linux環境
        • 2.Jenkins自動構建
          • 定時構建
          • 觸發構建
        • 3.NetCore部署
        • 4.總結一下

1.Linux環境

在進行自動構建之前,我們需要一個可以運行的Linux環境,並保證環境安裝了NetCoreSDK,Git,Jenkins等環境,那在這邊就不多說環境的安裝,提供相對應的安裝教程,大家按照安裝就可以,不過對Jenkins的安裝,我會多啰嗦一句。

環境 地址
NetCore https://dotnet.microsoft.com/download/linux-package-manager/centos/sdk-current
Git https://www.cnblogs.com/imyalost/p/8715688.html
Jenkins https://www.cnblogs.com/loveyouyou616/p/8714544.html
Supervisor https://www.cnblogs.com/miskis/p/6026452.html

啰嗦的那一句:
我在安裝Jenkins成功的時候進入網頁,網頁會一直保持在下面圖這個狀態,沒辦法進入到開始頁面,那解決辦法是:

Jenkins等待

  1. 進入到Jenkins的工作目錄/var/lib/jenkins/修改hudson.model.UpdateCenter.xml文件
  2. 將http://updates.jenkins-ci.org/update-center.json修改為http://mirror.xmission.com/jenkins/updates/update-center.json
  3. 重啟下Jenkins

2.Jenkins自動構建

  • 定時構建
    1. 創建Jenkins項目

    2. 配置好自定義工作目錄

    3. 填寫源代碼路徑

    4. 配置定時任務

      字段 * * * * *
      含義 分鐘 小時 日期 月份 星期
      取值範圍 0-59 0-23 1-31 1-12 0-7
      示例
      每隔15分鐘執行一次 H/15 * * * *
      每隔2個小時執行一次 H H/2 * * *
      每隔3天執行一次 H H H/3 * *
      每隔3天執行一次(每月的1-15號) H H 1-15/3 * *
      每周1,3,5執行一次 H H * * 1,3,5
      規則
      指定時間範圍 a-b
      指定時間間隔 /
      指定變量取值 a,b,c
    5. 配置構建命令

      1. dotnet restore 
      2. dotnet build 
      3. dotnet publish 
    6. 查看構建,並運行NetCore
      注意:
      因為你建立的自定義工作目錄,對於”jenkins”沒有讀寫權限,所以可能就會導致構建失敗,執行下面命令,給Jenkins賦予讀寫權限。

      1. chown jenkins /ftpfile -R  
      2. chmod 777 /ftpfile -R 

  • 觸發構建
    1. 配置GitHub
      針對我們的項目,我們要在自己的項目內添加webhook,並配置好webhook的地址。

      1. 在項目設置中找到webhook

      2. 設置Jenkins的hook地址到剛才添加的webhook中,

        Jenkins的hook地址為:http://你的地址(端口)/github-webhook,當然這個地址應該是外網可以訪問的

    2. Personal access tokens
      對於任何第三方訪問GitHub的項目,是需要授權的,Jenkins也是一樣的,所以我們需要先獲取一個accessToken

      1. 進入Settings頁面,找到左邊列表的Developer settings,點擊進入,可以看到如下:
      2. 點擊生成,然後勾選,,保存即可,就可以獲得token,順便說下,token要記下來,不然關閉頁面就看不到了。
    3. Jenkins配置
      上面的配置完成之後,我們需要返回Jenkins,進行再次設置。為Jenkins添加GitHub服務器。

      1. 添加GitHub服務器

        2.添加GitHub的憑據,也就我們剛才拿到得accessToken

    4. Jenkins任務配置
      因為我們已經將觸發構建的前置步驟都做完了,那接着我們就要去修改之前的定時構建的配置了

      1. 切換成觸發構建
      2. 使用密文模式,並添加綁定,選擇剛才添加的憑據
    5. 修改代碼,上傳GitHub
      終於我們將之前的定時構建改成了觸發構建,也就是我們每次Push代碼都會觸發構建,接下來我們試下。

      1. 提交代碼
      2. 查看Jenkins

3.NetCore部署

終於的終於,我們把構建這個步驟做好了,那麼接下來當然就是要部署我們網站咯。

  • 運行NetCore
    運行NetCore最簡單了,我們只需要在發布后的目錄運行dotnet ***.dll就可以了,當然,前提,你要裝CoreSDK。

  • nginx託管
    雖然說上面那樣已經運行起來,但是由於我們的環境在騰旭雲上,要訪問可以用nginx進行方向代理下,下面就簡單貼下配置就可以了。

    1. server { 
    2. listen 80; 
    3. location / { 
    4. proxy_pass http://localhost:5000; 
    5. proxy_http_version 1.1; 
    6. proxy_set_header Upgrade $http_upgrade; 
    7. proxy_set_header Connection keep-alive; 
    8. proxy_set_header Host $host; 
    9. proxy_cache_bypass $http_upgrade; 
    10. } 
    11. } 
  • 守護進程
    我們已經知道要運行Core,需要使用命令在控制台運行,但是一旦退出了,Core自然就退出了,所以我們在Linux底下需要一個類似IIS的,來託管我們運行Core的進程,守護進程,讓其在後台運行,自動重啟等等功能,這個就是supervisor

    supervisors是C/S架構的進程控制系統,可使用戶在類UNIX系統中監控、管理進程。常用於管理與某個用戶或項目相關的進程。

    安裝教程可以參考開始表格supervisor的鏈接,同樣怎麼對於怎麼對Core進行守護進程,文章內也有講,一步一步按照教程來就可以了。不過我這邊還是會講下我在使用supervisor中遇到的坑。

    1. 一開始,我百度了挺多的,發現百度中的文章,對於supervisor的日誌文件,很多都說在/etc/log/supervisor/supervisord.log這個文件內,但其實我去找的時候,發現並沒有,查看配置文檔,發現默認是在tmp中,我不知道是centeros 的版本問題,還是supervisor版本問題。

    2. 還有一個就是,在根據教程走完,配置后,要進行啟動時,可能會出現Unlinking stale socket /tmp/supervisor.sock 這個錯誤,我們只需要解鎖下就可以了unlink /tmp/supervisor.sock

4.總結一下

該篇文章簡單介紹了下Linux下Jenkins與GitHub自動構建NetCore與部署,很多細點我可能沒講出來,我把大大的教程貼出來,按照教程走起,就可以了,這也是我實踐過了的,肯定可以用的,非常感謝大大們,然後呢,我也想拋磚引玉下,在我Jenkins構建時有兩個問題,不知道誰可以幫我解答下。

  1. 比如我們的解決方案文件.sln並沒有在git項目的根目錄下,我們要指定需要構建的解決方案呢?

  2. 還有一個就是,在命令dotnet publish我們怎麼指定發布到另一個文件夾內呢,我知道有個參數-o|--output <OUTPUT_DIRECTORY>,我試了下,發現不行 dotnet publish -o /ftpfile/netCore/netCoreJenkins/JenkinsNetCore,會有以下的錯誤。

最後的最後,我的下一篇文章是《Linux下Jenkins與GitHub自動構建Node項目(Vue)》,期待0.5下吧。

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

進程知多少?

目錄

  • 進程為什麼出現?
  • 進程的組成
  • 如何競爭資源(調度算法)
    • FCFS
    • RR
    • SPN
    • SRT
    • HRRN
    • FB
  • 進程狀態
    • 三態圖
    • 五態圖
    • 七態圖
  • 進程關係
    • 父子關係
    • 殭屍進程
    • 孤兒進程
  • 執行模式
  • 進程間通訊
    • 管道(Pipe)
    • 流管道(Flow Pipe)
    • 有名管道(Named Pipe)
    • 信號量(Semophore)
    • 信號(Signal)
    • 消息隊列(Message Queue)
    • 共享內存(Shared Memory)
    • 套接字(Socket)
  • 總結

文章首發:進程知多少?

Java 多線程系列文章第 1 篇

要講線程,一般都得講一講進程,進程是何方神聖呢?下面來簡單介紹一下。

先通過任務管理器看看 Windows 系統下的進程。

從圖片來看,每一個進程都佔有 CPU、內存、磁盤、網絡等資源。站在操作系統的角度,進程是分配資源的基本單位,也是最小單位。

進程為什麼出現?

引入進程的目的:為了使多個程序能併發執行,以提高資源的利用率和系統的吞吐量。怎麼理解這句話呢?一個程序在運行過程中會涉及很多操作,利用 CPU 計算、通過磁盤 IO 進行數據傳輸等等,我們知道當程序在進行磁盤 IO 的時候,因為速度問題,會比較慢,所在在這個過程中 CPU 會空閑下來,這會造成資源的浪費,正因為引入進程,在 A 進程進行磁盤 IO 的時候,會讓出 CPU 給 B 進程,合理地利用了 CPU 資源,使得程序之間可以併發執行。

從 CPU 角度,執行過程是這樣子的:CPU 一直在負責執行指令,進程之間互相競爭 CPU 資源,下圖有 A 和 B 進程,在一個時間點,CPU 只執行一個進程的指令,因為 CPU 運行很快,所以在咱們看起來,像是多個進程在同時跑。這就是進程帶來的好處:提高資源利用率,併發地執行多個程序。

當然引入進程也不是有益無害,它增加了系統的時間和空間開銷。空間開銷這個好理解,進程有自己的組成部分(下面會講),這個就佔用了空間。時間開銷則是進程切換需要時間。

進程的組成

進程由 3 個部分組成,分別是程序代碼、數據集、棧和進程控制塊(Process Control Block)。

各自的作用如下:

  1. 程序代碼:描述了進程需要完成的功能。
  2. 數據集、棧:程序在執行時所需要的數據和工作區。
  3. 進程控制塊:包含進程的描述信息和控制信息,它是進程存在的唯一標識。

如何競爭資源(調度算法)

進程之間需要競爭資源,一般都是競爭 CPU 資源,因為 CPU 運行速度太快了,其他介質都趕不上。有了競爭就需要有規則,就像遊戲一樣,每個遊戲都需要規則,不同規則會有不同的側重點,這個看過“最強大腦”這個節目的朋友就非常清楚,每道題都有不同的考核側重點,有些是側重空間思維、有些側重邏輯推算等等。下面我們就簡單地一一講解競爭資源的遊戲規則。

FCFS

First In First Out(先來先服務):最先進入就緒隊列的進程,先運行,運行到完成或者阻塞時,再重新調度。一般情況下,這種調度算法會和優先級策略結合,比如每個優先級一條隊列,每條隊列中的調度都使用 FCFS。

特點:簡單、比較偏於長進程、相對於其他調度算法平均周轉時間長。

RR

Round Robin(輪轉):進程按提交順序存在就緒隊列,依次輪流佔用 CPU 資源,運行一段固定的時間,時間到后如果還沒執行完,就繼續進入就緒隊列隊尾,排隊等待下次執行。

特點:公平、對進程的響應時間較短。

SPN

Shortest Process Next(最短進程優先):將預期佔用運行時間最短的進程優先執行,直到運行完成或阻塞時,再重新調度。

特點:有利於短進程。

SRT

Shortest Remaining Time(最短剩餘時間優先):新進程進來時,如果新進程的預計運行時間比當前進程的剩餘運行時間更短,就搶佔當前進程,

特點:有利於短進程,和 SPN 的差別在於搶佔這個一點,因為搶佔,所以效率會比 SPN 好一些。

HRRN

Highest Response Ratio Next(最高響應比優先):當前運行的進程完成或者阻塞時發生調度,每次調度前,計算所有就緒進程的響應比,響應比高的進程優先運行。

響應比公式如下所示:

特點:有利於短進程,服務時間相同的進程,先來的服務會優先執行,長進程因為在等待的過程中,優先級越來越高,所以不會一直不執行。

FB

Feedback (反饋):由多個就緒隊列組成的反饋機制,它有如下規則:

  1. 在同一個隊列的進程,按 FCFS 算法調度,最後一個就緒隊列按 RR 算法調度;
  2. 優先級越高的隊列,時間片越小;
  3. 進程在一個時間片內未運行完,則降到下一個隊列末尾;
  4. 只有上級隊列無就緒進程時,才運行本級就緒隊列,本級就緒隊列無進程時,才運行下級就緒隊列,以此類推

進程執行過程如下圖所示

特點:短進程有非常大的優勢,排在前面的隊列都是時間較短的。

以上就是幾個搶佔資源的調度算法的說明。

進程狀態

上面我們講到,進程之間是在競爭資源,得到資源就運行,沒得到就等待,這個需要有狀態來維護,像很多系統一樣,需要一個狀態機。

三態圖

三態圖也是描述進程狀態最簡單最基礎的圖,它包含了進程的最基本的 3 個狀態,分別是:就緒態、運行態和阻塞態。

Read(就緒態):進程已得到除 CPU 以外的其他所需資源。
Running(運行態):進程的指令正被執行。
Blocked(阻塞態):進程正等待資源或某事件發生。

就緒態的進程在被調度的時候,進入了運行態,如果時間片運行完或者有更高級別進程搶佔資源,則變成就緒態等待再次被調度;如果發生事件(比如 IO 事件),則從運行態轉到阻塞態,進入阻塞態的進程只能等待事件解除重新進入就緒態。

五態圖

基於三態圖,新增了 2 個狀態,分別是:新建態和退出態。

New(新建態):進程正被創建。分配內存后將被設為就緒態。

Exit(退出態):進程已正常結束或出現異常結束。回收資源。

新進程剛創建還沒有分配資源的時候是新建態,等到分配了資源,被加載后就進入就緒態。當進程運行完后,就從運行態進入退出態。

七態圖

基於五態圖,新增了 2 種掛起態,分別是就緒掛起態和阻塞掛起態。

就緒掛起態:另叫外存就緒態。由於內存容量有限,將原位於內存的就緒進程轉存到外存(磁盤)上。

阻塞掛起態:另叫外存阻塞態。一樣因為內存容量有限,將原位於內存的阻塞進程轉存到外存(磁盤)上。

我們可以看出,圖中新增了解除掛起的狀態轉換過程,一般是由於掛起進程優先級比較高或者內存空間足夠,把位於外存(磁盤)的進程轉存到內存中。

進程關係

進程之間其實比較獨立,比如我們在日常使用的 QQ 和微信,它們運行起來的進程有什麼關係么?其實除了互相競爭資源之外,沒有任何關係。

父子關係

雖然上面說的進程之間沒有關係,但是有一個特殊關係需要講,就是父子關係。

先做個試驗,驗證進程的父子關係。操作步驟:

  1. 打開 CMD 命令行程序,將當前的窗口設置為 Father,在 Father 窗口通過命令start cmd啟動另一個 CMD 命令行程序;
  2. 將新開的 CMD 命令行程序的窗口設置為 Son,在 Son 窗口通過命令start cmd啟動另一個 CMD 命令行程序;
  3. 將新開的 CMD 命令行程序的窗口設置為 Grandson。

操作過程如下圖所示。

通過 ProcessExplorer 可以很清晰看到這 3 個 CMD 進程之間的關係。(想要 ProcessExplorer 插件可以通過百度網盤下載鏈接:https://pan.baidu.com/s/19531gf5tD_of1CWxpFR9Dg 提取碼:qhc6)

我們看到 Father、Son、Grandson 三個進程呈現出我們預料中的樹形。那麼什麼是父子進程呢?簡單的說就是在進程中創建出新的進程,這個新的進程就是子進程,一個進程可以有多個子進程,但是只能有一個父進程。在 Unix 系統中,父進程通過調用 fork() 創建子進程,父子進程有如下特點:

  1. 父、子進程併發執行;
  2. 父、子進程共享父進程的所有資源;
  3. 子進程複製父進程的地址空間,甚至有相同的正文段和程序計數器 PC 值;
  4. 利用寫時複製(Copy On Write)技術減少不必要的複製:fork 時父子共用父空間,當一方試圖修改時才複製。

這裏重點講一下Copy On Write,使用了這個技術,父進程創建子進程的時候不會複製所有數據到子進程,省了複製的時間以及減少了大量的內存。這個複製不是必要的,因為如果應用程序在進程複製之後立即加載新程序,那之前的複製工作就是浪費時間和內存了。

講了進程父子關係,就免不了提一下殭屍進程和孤兒進程,下面分別介紹一下。

殭屍進程

殭屍進程:子進程退出后,父進程沒有調用 wait 或 waitpid 獲取子進程的狀態信息,子進程的進程描述符仍保存在系統中,這種進程叫殭屍進程。

殭屍進程的危害:殭屍進程會一直佔用進程號,系統能使用的進程號又是有限的,如果有大量的殭屍進程,會因為沒有可用進程號導致無法創建新的進程。

孤兒進程

孤兒進程:父進程結束退出,而它的子進程還在運行,這時的子進程就叫做孤兒進程。孤兒進程就被 init 進程(進程號為 1)收養,init 進程將對孤兒進程完成狀態收集工作。

孤兒進程沒有危害,因為被 init 進程託管了,init 進程會處理孤兒進程的收集工作。

執行模式

指令分為特權指令(只能由操作系統內核使用的指令)和非特權指令(只能由用戶程序使用的指令),因為指令有特權和非特權之分,所以 CPU 也分為 2 種執行模式:系統態(可以執行所有指令,使用所有資源以及改變 CPU 狀態)和用戶態(只能執行非特權指令)。

CPU 的系統態和用戶態之間的切換。

進程間通訊

當進程之間需要數據傳輸、共享數據時,進程間就需要互相通訊,通訊方式有如下幾種,這裏只是簡單概括一下,不展開講,咱的重點在於多線程,進程咱們簡單了解一下就可以,感興趣的同學可以根據要點進行深入學習。

管道(Pipe)

管道是半雙工通訊,數據是單向流動,要建立進程間互相通訊,則需要 2 個管道,這種通訊方式只能在親戚關係的進程間使用,比如父子進程。

流管道(Flow Pipe)

流管道是管道進化來的,數據不再是單向流動,可以雙向流動,但是依舊是只能在親戚關係的進程間使用。

有名管道(Named Pipe)

有名管道提供了新的功能,就是給管道設置名字,它改善了上面 2 種管道通訊方式,支持了非親戚關係的進程通訊。

信號量(Semophore)

信號量相當於計數器,利用它來控制多個進程訪問共享資源,當一個進程A在訪問共享資源時,信號量防止其他進程來訪問,只有當進程A不訪問共享資源了,其他進程才能訪問。

信號(Signal)

信號可以在任何時候發給某一進程,不需要知道該進程當前的狀態,如果對方進程未執行,信號會存在內核中,直到進程執行後傳遞給它;如果對方進程是阻塞,則信號會延遲傳遞,等到對方進程阻塞取消后才傳遞給它。

消息隊列(Message Queue)

消息隊列是存放在內核中的鏈表,可以有多個進程對這個鏈表進行寫入和讀取,它解決了信號傳遞信息少、管道只能傳輸無格式字節流和緩衝區大小受限的缺點。目前有 POSIX 消息隊列和 System V 消息隊列。

共享內存(Shared Memory)

共享內存即為一段能被其他進程訪問的內存,多個進程訪問同一個內存,達到了通訊的效果。

套接字(Socket)

套接字就是我們網絡編程裏面的那個套接字,可以通過網絡也可以在本機進行通信,它的好處在於可以跨主機進行通信。

總結

總的來說,進程是程序在一個數據集上的一次執行過程,它就是程序運行起來的表現。這是我們學習多線程的開篇,希望通過這篇文章,讓大家簡單地了解進程是什麼,後面我們再來深入了解多線程。

推薦閱讀

設計模式看了又忘,忘了又看?

公眾號後台回復『設計模式』可以獲取《一故事一設計模式》电子書

覺得文章有用幫忙轉發&點贊,多謝朋友們!

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!

Express:模板引擎深入研究

深入源碼

首先,看下express模板默認配置。

  • view:模板引擎模塊,對應 require(‘./view’),結合 res.render(name) 更好了解些。下面會看下 view 模塊。
  • views:模板路徑,默認在 views 目錄下。
// default configuration
this.set('view', View);
this.set('views', resolve('views'));

騰訊IVWEB前端團隊招前端工程師,2年以上工作經驗,本科以上學歷,有意者可私信、留言,或者郵箱聯繫 2377488447@qq.com,JD可參考這裏

從實例出發

從官方腳手架生成的代碼出發,模板配置如下:

  • views:模板文件在 views 目錄下;
  • view engine:用jade這個模板引擎進行模板渲染;
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

假設此時有如下代碼調用,內部邏輯是如何實現的?

res.render('index');

res.render(view)

完整的 render 方法代碼如下:

/**
 * Render `view` with the given `options` and optional callback `fn`.
 * When a callback function is given a response will _not_ be made
 * automatically, otherwise a response of _200_ and _text/html_ is given.
 *
 * Options:
 *
 *  - `cache`     boolean hinting to the engine it should cache
 *  - `filename`  filename of the view being rendered
 *
 * @public
 */

res.render = function render(view, options, callback) {
  var app = this.req.app;
  var done = callback;
  var opts = options || {};
  var req = this.req;
  var self = this;

  // support callback function as second arg
  if (typeof options === 'function') {
    done = options;
    opts = {};
  }

  // merge res.locals
  opts._locals = self.locals;

  // default callback to respond
  done = done || function (err, str) {
    if (err) return req.next(err);
    self.send(str);
  };

  // render
  app.render(view, opts, done);
};

核心代碼就一句,調用了 app.render(view) 這個方法。

res.render = function (name, options, callback) {
  var app = this.req.app;
  app.render(view, opts, done);
};

app.render(view)

完整源碼如下:

/**
 * Render the given view `name` name with `options`
 * and a callback accepting an error and the
 * rendered template string.
 *
 * Example:
 *
 *    app.render('email', { name: 'Tobi' }, function(err, html){
 *      // ...
 *    })
 *
 * @param {String} name
 * @param {String|Function} options or fn
 * @param {Function} callback
 * @public
 */

app.render = function render(name, options, callback) {
  var cache = this.cache;
  var done = callback;
  var engines = this.engines;
  var opts = options;
  var renderOptions = {};
  var view;

  // support callback function as second arg
  if (typeof options === 'function') {
    done = options;
    opts = {};
  }

  // merge app.locals
  merge(renderOptions, this.locals);

  // merge options._locals
  if (opts._locals) {
    merge(renderOptions, opts._locals);
  }

  // merge options
  merge(renderOptions, opts);

  // set .cache unless explicitly provided
  if (renderOptions.cache == null) {
    renderOptions.cache = this.enabled('view cache');
  }

  // primed cache
  if (renderOptions.cache) {
    view = cache[name];
  }

  // view
  if (!view) {
    var View = this.get('view');

    view = new View(name, {
      defaultEngine: this.get('view engine'),
      root: this.get('views'),
      engines: engines
    });

    if (!view.path) {
      var dirs = Array.isArray(view.root) && view.root.length > 1
        ? 'directories "' + view.root.slice(0, -1).join('", "') + '" or "' + view.root[view.root.length - 1] + '"'
        : 'directory "' + view.root + '"'
      var err = new Error('Failed to lookup view "' + name + '" in views ' + dirs);
      err.view = view;
      return done(err);
    }

    // prime the cache
    if (renderOptions.cache) {
      cache[name] = view;
    }
  }

  // render
  tryRender(view, renderOptions, done);
};

源碼開頭有 cache、engines 兩個屬性,它們在 app.int() 階段就初始化了。

this.cache = {};
this.engines = {};

View模塊源碼

看下View模塊的源碼:

/**
 * Initialize a new `View` with the given `name`.
 *
 * Options:
 *
 *   - `defaultEngine` the default template engine name
 *   - `engines` template engine require() cache
 *   - `root` root path for view lookup
 *
 * @param {string} name
 * @param {object} options
 * @public
 */

function View(name, options) {
  var opts = options || {};

  this.defaultEngine = opts.defaultEngine;
  this.ext = extname(name);
  this.name = name;
  this.root = opts.root;

  if (!this.ext && !this.defaultEngine) {
    throw new Error('No default engine was specified and no extension was provided.');
  }

  var fileName = name;

  if (!this.ext) {
    // get extension from default engine name
    this.ext = this.defaultEngine[0] !== '.'
      ? '.' + this.defaultEngine
      : this.defaultEngine;

    fileName += this.ext;
  }

  if (!opts.engines[this.ext]) {
    // load engine
    opts.engines[this.ext] = require(this.ext.substr(1)).__express;
  }

  // store loaded engine
  this.engine = opts.engines[this.ext];

  // lookup path
  this.path = this.lookup(fileName);
}

核心概念:模板引擎

模板引擎大家不陌生了,關於express模板引擎的介紹可以參考官方文檔。

下面主要講下使用配置、選型等方面的內容。

可選的模版引擎

包括但不限於如下模板引擎

  • jade
  • ejs
  • dust.js
  • dot
  • mustache
  • handlerbar
  • nunjunks

配置說明

先看代碼。

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

有兩個關於模版引擎的配置:

  1. views:模版文件放在哪裡,默認是在項目根目錄下。舉個例子:app.set('views', './views')
  2. view engine:使用什麼模版引擎,舉例:app.set('view engine', 'jade')

可以看到,默認是用jade做模版的。如果不想用jade怎麼辦呢?下面會提供一些模板引擎選擇的思路。

選擇標準

需要考慮兩點:實際業務需求、個人偏好。

首先考慮業務需求,需要支持以下幾點特性。

  • 支持模版繼承(extend)
  • 支持模版擴展(block)
  • 支持模版組合(include)
  • 支持預編譯

對比了下,jade、nunjunks都滿足要求。個人更習慣nunjunks的風格,於是敲定。那麼,怎麼樣使用呢?

支持nunjucks

首先,安裝依賴

npm install --save nunjucks

然後,添加如下配置

var nunjucks = require('nunjucks');

nunjucks.configure('views', {
    autoescape: true,
    express: app
});

app.set('view engine', 'html');

看下views/layout.html

<!DOCTYPE html>
<html>
<head>
    <title>
        {% block title %}
            layout title
        {% endblock %}
    </title>
</head>
<body>
<h1>
    {% block appTitle %}
        layout app title
    {% endblock %}
</h1>
<p>正文</p>

</body>
</html>

看下views/index.html

{% extends "layout.html" %}
{% block title %}首頁{% endblock %}
{% block appTitle %}首頁{% endblock %}

開發模板引擎

通過app.engine(engineExt, engineFunc)來註冊模板引擎。其中

  • engineExt:模板文件後綴名。比如jade。
  • engineFunc:模板引擎核心邏輯的定義,一個帶三個參數的函數(如下)
// filepath: 模板文件的路徑
// options:渲染模板所用的參數
// callback:渲染完成回調
app.engine(engineExt, function(filepath, options, callback){

    // 參數一:渲染過程的錯誤,如成功,則為null
    // 參數二:渲染出來的字符串
    return callback(null, 'Hello World');
});

比如下面例子,註冊模板引擎 + 修改配置一起,於是就可以愉快的使用後綴為tmpl的模板引擎了。

app.engine('tmpl', function(filepath, options, callback){

    // 參數一:渲染過程的錯誤,如成功,則為null
    // 參數二:渲染出來的字符串
    return callback(null, 'Hello World');
});
app.set('views', './views');
app.set('view engine', 'tmpl');

res.render(view [, locals] [, callback])

參數說明:

  • view:模板的路徑。
  • locals:對象類型。渲染模板時傳進去的本地變量。
  • callback:回調函數。如果聲明了的話,當渲染工作完成時被調用,參數為兩個,分別是錯誤(如果出錯的話)、渲染好的字符串。在這種情況下,response不會自動完成。當錯誤發生時,內部會自動調用 next(err)

view參數說明:

  • 可以是相對路徑(相對於views設置的目錄),或者絕對路徑;
  • 如果沒有聲明文件後綴,則以view engine設置為準;
  • 如果聲明了文件後綴,那麼Express會根據文件後綴,通過 require() 加載對應的模板引擎來完成渲染工作(通過模板引擎的 __express 方法完成渲染)。

locals參數說明:

locals.cache 啟動模板緩存。在生產環境中,模板緩存是默認啟用的。在開發環境,可以通過將 locals.cache 設置為true來啟用模板緩存。

例子:

// send the rendered view to the client
res.render('index');

// if a callback is specified, the rendered HTML string has to be sent explicitly
res.render('index', function(err, html) {
  res.send(html);
});

// pass a local variable to the view
res.render('user', { name: 'Tobi' }, function(err, html) {
  // ...
});

關於view cache

The local variable cache enables view caching. Set it to true, to cache the view during development; view caching is enabled in production by default.
render(view, opt, callback) 這個方法調用時,Express會根據 view 的值 ,進行如下操作

  1. 確定模板的路徑
  2. 根據模板的擴展性確定採用哪個渲染引擎
  3. 加載渲染引擎

重複調用render()方法,如果 cache === false 那麼上面的步驟每次都會重新做一遍;如果 cache === true,那麼上面的步驟會跳過;

關鍵源代碼:

if (renderOptions.cache) {
  view = cache[name];
}

此外,在 view.render(options, callback) 里,options 也會作為參數傳入this.engine(this.path, options, callback)。也就是說,渲染引擎(比如jade)也會讀取到options.cache這個配置。根據options.cache的值,渲染引擎內部也可能會進行緩存操作。(比如為true時,jade讀取模板後會緩存起來,如果為false,每次都會重新從文件系統讀取)

View.prototype.render = function render(options, callback) {
  debug('render "%s"', this.path);
  this.engine(this.path, options, callback);
};

備註:cache配置對渲染引擎的影響是不確定的,因此實際需要用到某個渲染引擎時,需確保對渲染引擎足夠了解。

以jade為例,在開發階段,NODE_ENV !== 'production',cahce默認是false。因此每次都會從文件系統讀取模板,再進行渲染。因此,在開發階段,可以動態修改模板內容來查看效果。

當 NODE_ENV === 'production' ,cache 默認是true,此時會緩存模板,提升性能。

混合使用多種模板引擎

根據對源碼的分析,實現很簡單。只要帶上文件擴展名,Express就會根據擴展名加載相應的模板引擎。比如:

  1. index.jade:加載引擎jade
  2. index.ejs:加載引擎ejss
// 混合使用多種模板引擎
var express = require('express');
var app = express();

app.get('/index.jade', function (req, res, next) {
  res.render('index.jade', {title: 'jade'});
});

app.get('/index.ejs', function (req, res, next) {
  res.render('index.ejs', {title: 'ejs'});
});

app.listen(3000);

同樣的模板引擎,不同的文件擴展名

比如模板引擎是jade,但是因為一些原因,擴展名需要採用.tpl

// 同樣的模板引擎,不同的擴展名
var express = require('express');
var app = express();

// 模板採用 tpl 擴展名
app.set('view engine', 'tpl');
// 對於以 tpl 擴展名結尾的模板,採用 jade 引擎
app.engine('tpl', require('jade').__express);

app.get('/index', function (req, res, next) {
  res.render('index', {title: 'tpl'});
});

app.listen(3000);

相關鏈接

Using template engines with Express
http://expressjs.com/en/guide/using-template-engines.html

res.render 方法使用說明
http://expressjs.com/en/4x/api.html#res.render

騰訊IVWEB前端團隊招前端工程師,2年以上工作經驗,本科以上學歷,有意者可私信、留言,或者郵箱聯繫 2377488447@qq.com,JD可參考這裏

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司、台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師"嚨底家"!!