解讀三組容易混淆的Dockerfile指令

長話短說,今天分享三組容易混淆的Dockerfile指令, 幫助大家編寫更優雅的Dockfile文件、構建更純凈的Docker鏡像。

COPY vs ADD

COPY、ADD主體功能類似:從指定位置拷貝文件到Docker鏡像。

COPY <src>... <dest>
ADD <src>... <dest>

COPY 接收src、dest參數,只允許從Docker Engine主機上拷貝文件到Docker鏡像;
ADD也能完成以上工作,但是ADD支持另外兩種src:

  1. 文件源可以是URL
  2. 可以從src直接解壓tar文件到目的地
ADD http://foo.com/bar.go /tmp/main.go
# 從指定地址下載文件,添加到鏡像文件系統的/tmp/main.go位置
ADD http://foo.com/bar.go /tmp/
# 因為以/結尾,將會引用url中的文件名添加到指定的目錄下


ADD /foo.tar.gz /tmp/
# 自動解壓主機文件到指定目錄

有趣的是,URL下載和自動解壓功能不能同時生效: 任何通過URL下載的壓縮包文件不會自動解壓。

  • 如果拷貝本地文件到鏡像,通常使用COPY,因為含義更明確
  • ADD支持URL文件、自動解壓到指定目錄,這2個特性也很棒

ARG vs ENV

ARG、ENV也讓人很疑惑的,都是Dockerfile中定義變量的指令。

ARG用於鏡像構建階段,ENV用於將來運行的容器

  • 生成鏡像后,ARG值不可用,正在運行的容器將無法訪問ARG變量值。
ARG  VAR_NAME 5
# 構建鏡像時,可提供--build-arg  VAR_NAME=6修改ARG值。
  • ENV主要是為容器環境變量提供默認值,正在運行的容器可訪問環境變量(這是將配置傳遞給應用的好方法):
ENV VAR_NAME_2 6
# 啟動容器時,可通過docker run -e "VAR_NAME_2=7"或docker-compose.yml提供新的環境變量值來覆蓋Dockerfile中設置的ENV值。

一個小技巧: 構建鏡像時不能使用命令行參數重寫ENV,但是你可以使用ARG動態為ENV設置默認值:

# You can set VAR_A while building the image or leave it at the default
ARG VAR_A 5
# VAR_B gets the (overridden) value of VAR_A
ENV VAR_B $VAR_A

RUN vs ENTRYPOINT vs CMD

  1. RUN 在新層中執行命令併產生新鏡像,主要用於安裝新軟件包。
  2. ENTRYPOINT 執行程序的啟動命令,當您想將容器作為可執行文件運行時使用。
  3. CMD和ENTRYPOINT 都可以提供程序的啟動命令;CMD另外一個作用是為執行中的容器提供默認值
  • CMD [“executable”,”param1″,”param2″] (可執行形式,最常見)
  • CMD command param1 param2 (腳本形式)
CMD echo "Hello world"
# run -it <image> 輸出 Hello world

但是當容器以命令啟動,docker run -it /bin/bash, CMD命令會被忽略,bash解析器將會運行:root@98e4bed87725:/#

  • CMD [“param1″,”param2”] (作為ENTRYPOINT指令默認值,此時必須提供ENTRYPOINT指令,且ENTRYPOINT也必須以Json Array形式)
ENTRYPOINT ["/bin/echo", "Hello"]  
CMD ["world"]  

# run -it <image> 將會輸出 Hello world;
# run -it <image> earth 將會輸出 Hello earth

當打算構建一個可執行的且常駐的鏡像,最好選用ENTRYPOINT;
如果需要提供默認命令參數(可在容器運行時從命令行覆蓋),請選擇CMD

Reference

  • https://www.ctl.io/developers/blog/post/dockerfile-add-vs-copy/
  • https://vsupalov.com/docker-arg-vs-env/
  • https://aboullaite.me/dockerfile-run-vs-cmd-vs-entrypoint/

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

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

網頁設計最專業,超強功能平台可客製化

淺談鏈接器

目錄

  • 編譯過程簡介
  • 什麼是鏈接器?
  • 鏈接器可操作的元素:目標文件
  • 符號表(Symbol table)
    • 符號決議
  • 庫與可執行文件
    • 靜態庫
    • 動態庫
  • 參考
  • 微信公共號

編譯過程簡介

C語言的編譯過程由五個階段組成:

  • 步驟1:預處理:主要是處理以#開頭的語句,主要工作如下:1)將#include包含的頭文件直接拷貝到.c文件中;2)將#define定義的宏進行替換;3)處理條件編譯指令#ifdef;4)將代碼中的註釋刪除;5)添加行號和文件標示,這樣的在調試和編譯出錯的時候才知道是是哪個文件的哪一行 ;6)保留#pragma編譯器指令,因為編譯器需要使用它們。
gcc -E helloworld.c -o helloworld_pre.c
  • 步驟2: 編譯:將C語言翻譯成彙編,主要工作如下:1)詞法分析;2)語法分析;3)語義分析 4)優化後生成相應的彙編;
gcc -S helloworld.c -o helloworld.s
  • 步驟3: 彙編:將上一步的彙編代碼轉換成機器碼(machine code),這一步產生的文件叫做目標文件;
gcc -c helloworld.c -o helloworld.o
  • 步驟4:鏈接:將多個目標文以及所需的庫文件(.so等)鏈接成最終的可執行文件(executable file)。
gcc helloworld.c -o helloworld

什麼是鏈接器?

鏈接器是一個將編譯器產生的目標文件打包成可執行文件或者庫文件或者目標文件的程序。

鏈接器的作用有點類似於我們經常使用的壓縮軟WinRAR(Linux下是tar),壓縮軟件將一堆文件打包壓縮成一個壓縮文件,而鏈接器和壓縮軟件的區別在於鏈接器是將多個目標文件打包成一個文件而不進行壓縮。

寫C或者C++的u同學經常遇到這樣一個錯誤:

undefined reference to function ABC.

鏈接器可操作的元素:目標文件

鏈接器可操作的最小元素是一個簡單的目標文件
從廣義上來講,目標文件與可執行文件的格式幾乎是一模一樣的,在Linux下,我們把它們統稱為ELF文件。

ELF文件標準裏面把系統中採用ELF格式的文件歸為以下四類:

  • 可重定位文件(Relocatable File):Linux的.o文件,這類文件包含了代碼和數據,可以被用來鏈接成可執行文件或共享目標文件,靜態鏈接庫也歸屬於這一類;

  • 可執行文件(Executable File):比如bin/bash文件,這類文件包含了可以直接執行的程序,它的代表就是ELF文件,他們一般都沒有擴展名;

  • 共享目標文件(shared Object File): 比如Linux的.so文件,這種文件包含了代碼和數據,可以在以下兩種情況下使用,一種是鏈接器可以直接使用這種文件跟其他的可重定位文件和共享目標文件鏈接,產生新的目標文件。第二種是動態鏈接器可以將幾個這樣的共享目標文件與可執行文件結合,作為進程映射的一部分來運行。

  • 核心轉儲文件(Core Dump File): Linux下面的core dump,當進程意外終止時,系統可以將該進程的地址空間的內容及終止時的一些其他信息轉儲到核心轉儲文件中。

符號表(Symbol table)

編譯器在遇到外部定義的全局變量或者函數時只要能在當前文件找到其聲明,編譯器就認為編譯正確。而尋找使用變量定義的這項任務就被留給了鏈接器。鏈接器的其中一項任務就是要確定所使用的變量要有其唯一的定義。雖然編譯器給鏈接器留了一項任務,但為了讓鏈接器工作的輕鬆一點編譯器還是多做了一點工作的,這部分工作就是符號表(Symbol table)。

符號表中保存的信息有兩個部分:

  • 該目標文件中引用的全局變量以及函數;
  • 該目標文件中定義的全局變量以及函數。

編譯器在編譯過程中每次遇到一個全局變量或者函數名都會在符號表中添加一項,最終編譯器會統計一張符號表。

假設C語言源碼如下:

// 定義未初始化的全局變量
int g_x_uninit;

// 定義初始化的全局變量
int g_x_init = 1;

// 定義未初始化的全局私有變量,只能在當前文件中使用
static int g_y_uninit;

// 定義初始化的全局私有變量
static int g_y_init = 2;

// 聲明全局變量,該變量的定義在其它文件
extern int g_z;

// 函數聲明,該函數的定義在其它文件
int fn_a(int x, int y);

// 私有函數定義,該函數只能在當前文件中使用
static int fn_b(int x)
{
    return x + 1;
}

// 函數定義
int fn_c(int local_x)
{
    int local_y_uninit;
    int local_y_init = 3;
    // 對全局變量,局部變量以及函數的使用
    g_x_uninit = fn_a(local_x, g_x_init);
    g_y_uninit = fn_a(local_x, local_y_init);
    local_y_uninit += fn_b(g_z);
    return (g_y_uninit + local_y_uninit);
}

編譯器將為此文件統計出如下一張符號表:

名字 類型 是否可被外部引用 區域
g_z 引用,未定義
fn_a 引用,未定義
fn_b 定義 代碼段
fn_c 定義 代碼段
g_x_init 定義 數據段
g_y_uninit 定義 數據段
g_x_uninit 定義 數據段
g_y_init 定義 數據段

g_z以及fn_a是未定義的,因為在當前文件中,這兩個變量僅僅是聲明,編譯器並沒有找到其定義。剩餘的變量編譯器都可以在當前文件中找到其定義。

本質上整個符號表主要表達兩件事:1)我能提供給其它文件使用的符號; 2)我需要其它文件提供給我使用的符號。

目標文件
數據段
代碼段
符號表

符號決議

有了符號表,鏈接器就可以進行符號決議了。如圖所示,假設鏈接器需要鏈接三個目標文件,如下:

鏈接器會依次掃描每一個給定的目標文件,同時鏈接器還維護了兩個集合,一個是已定義符號集合D,另一個是未定義符合集合U,下面是鏈接器進行符合決議的過程:

  • 對於當前目標文件,查找其符號表,並將已定義的符號並添加到已定義符號集合D中。
  • 對於當前目標文件,查找其符號表,將每一個當前目標文件引用的符號與已定義符號集合D進行對比,如果該符號不在集合D中則將其添加到未定義符合集合U中。
  • 當所有文件都掃描完成后,如果為定義符號集合U不為空,則說明當前輸入的目標文件集合中有未定義錯誤,鏈接器報錯,整個編譯過程終止。

鏈接過程中,只要每個目標文件所引用變量都能在其它目標文件中找到唯一的定義,整個鏈接過程就是正確的。

若鏈接器在查找了所有目標文件的符號表后都沒有找到函數,因此鏈接器停止工作並報出錯誤undefined reference to function A

庫與可執行文件

鏈接器根據目標文件構建出庫(動態庫、靜態庫)或可執行文件。

給定目標文件以及鏈接選項,鏈接器可以生成兩種庫,分別是靜態庫以及動態庫,如下圖所示,給定同樣的目標文件,鏈接器可以生成兩種不同類型的庫。

靜態庫

靜態庫在Windows下是以.lib為後綴的文件,Linux下是以.a為後綴的文件。

靜態庫是鏈接器通過靜態鏈接將其和其它目標文件合併生成可執行文件的,而靜態庫只不過是將多個目標文件進行了打包,在鏈接時只取靜態庫中所用到的目標文件。

目標文件分為三段:代碼段、數據段、符號表,在靜態鏈接時可執行文件的生成過程如下圖所示:

可執行文件的特點如下:

  • 可執行文件和目標文件一樣,也是由代碼段和數據段組成。
  • 每個目標文件中的數據段都合併到了可執行文件的數據段,每個目標文件當中的代碼段都合併到了可執行文件的代碼段。
  • 目標文件當中的符號表並沒有合併到可執行文件當中,因為可執行文件不需要這些字段。

可執行文件和目標文件沒有什麼本質的不同,可執行文件區別於目標文件的地方在於,可執行文件有一個入口函數,這個函數也就是我們在C語言當中定義的main函數,main函數在執行過程中會用到所有可執行文件當中的代碼和數據。main函數是被操作系統調用。

動態庫

靜態庫在編譯鏈接期間就被打包copy到了可執行文件,也就是說靜態庫其實是在編譯期間(Compile time)鏈接使用的。
動態鏈接可以在兩種情況下被鏈接使用,分別是加載時動態鏈接(load-time dynamic linking)運行時動態鏈接 (run-time dynamic linking)

  • 加載時動態鏈接:在這裏我們只需要簡單的把加載理解為程序從磁盤複製到內存的過程,加載時動態鏈接就出現在這個過程。操作系統會查找可執行文件依賴的動態庫信息(主要是動態庫的名字以及存放路徑),找到該動態庫后就將該動態庫從磁盤搬到內存,並進行符號決議,如果這個過程沒有問題,那麼一切準備工作就緒,程序就可以開始執行了,如果找不到相應的動態庫或者符號決議失敗,那麼會有相應的錯誤信息報告為用戶,程序運行失敗。

  • 運行時動態鏈接:run-time dynamic linking 運行時動態鏈接則不需要在編譯鏈接時提供動態庫信息,也就是說,在可執行文件被啟動運行之前,可執行文件對所依賴的動態庫信息一無所知,只有當程序運行到需要調用動態庫所提供的代碼時才會啟動動態鏈接過程。

可以使用特定的API來運行時加載動態庫,在Windows下通過LoadLibrary或者LoadLibraryEx,在Linux下通過使用dlopen、dlsym、dlclose這樣一組函數在運行時鏈接動態庫。當這些API被調用后,同樣是首先去找這些動態庫,將其從磁盤copy到內存,然後查找程序依賴的函數是否在動態庫中定義。這些過程完成后動態庫中的代碼就可以被正常使用了。

在動態鏈接下,可執行文件當中會新增兩段,即dynamic段以及GOT(Global offset table)段,這兩段內容就是是我們之前所說的必要信息。

dynamic 段中保存了可執行文件依賴哪些動態庫,動態鏈接符號表的位置以及重定位表的位置等信息。
當加載可執行文件時,操作系統根據dynamic段中的信息即可找到使用的動態庫,從而完成動態鏈接

參考

  • C語言編譯的4大過程詳解
  • C語言編程透視
  • 徹底理解鏈接器:二,符號決議

微信公共號

NFVschool,關注最前沿的網絡技術。

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

【其他文章推薦】

網頁設計最專業,超強功能平台可客製化

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

※教你寫出一流的銷售文案?

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家公司費用怎麼算?

一次依賴注入不慎引發的一連串事故

一次依賴注入不慎引發的一連串事故

起因和現象

偶爾會看到線上服務啟動的時候第一波流量進來之後,

遲遲沒有任何的響應,同時服務的監控檢查接口正常,

所以 K8S 集群認為服務正常,繼續放入流量。

查看日誌基本如下:


[2020-06-05T13:00:30.7080743+00:00 Microsoft.AspNetCore.Hosting.Diagnostics INF] Request starting HTTP/1.0 GET http://172.16.2.52/v1/user/test
[2020-06-05T13:00:30.7081525+00:00 Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware DBG] The request path /v1/user/test/account-balance does not match a supported file type
[2020-06-05T13:00:31.7074253+00:00 Microsoft.AspNetCore.Server.Kestrel DBG] Connection id "0HM09A1MAAR21" started.
[2020-06-05T13:00:31.7077051+00:00 Microsoft.AspNetCore.Hosting.Diagnostics INF] Request starting HTTP/1.0 GET http://172.16.2.52/v1/user/test/account-balance
[2020-06-05T13:00:31.7077942+00:00 Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware DBG] The request path /v1/user/test/account-balance does not match a supported file type
[2020-06-05T13:00:32.2103440+00:00 Microsoft.AspNetCore.Server.Kestrel DBG] Connection id "0HM09A1MAAR22" started.
[2020-06-05T13:00:32.2118432+00:00 Microsoft.AspNetCore.Hosting.Diagnostics INF] Request starting HTTP/1.0 GET http://172.16.2.52/v1/user/test/account-balance
[2020-06-05T13:00:32.2125894+00:00 Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware DBG] The request path /v1/user/test/account-ba'lan'ce does not match a supported file type
[2020-06-05T13:00:33.2223942+00:00 Microsoft.AspNetCore.Server.Kestrel DBG] Connection id "0HM09A1MAAR23" started.
[2020-06-05T13:00:33.2238736+00:00 Microsoft.AspNetCore.Hosting.Diagnostics INF] Request starting HTTP/1.0 GET http://172.16.2.52/v1/user/test/account-balance
[2020-06-05T13:00:33.2243808+00:00 Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware DBG] The request path /v1/user/test/account-balance does not match a supported file type
[2020-06-05T13:00:34.2177528+00:00 Microsoft.AspNetCore.Server.Kestrel DBG] Connection id "0HM09A1MAAR24" started.
[2020-06-05T13:00:34.2189073+00:00 Microsoft.AspNetCore.Hosting.Diagnostics INF] Request starting HTTP/1.0 GET http://172.16.2.52/v1/user/test/account-balance
[2020-06-05T13:00:34.2193483+00:00 Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware DBG] The request path /v1/user/test/account-balance does not match a supported file type
[2020-06-05T13:00:35.2169806+00:00 Microsoft.AspNetCore.Server.Kestrel DBG] Connection id "0HM09A1MAAR25" started.
[2020-06-05T13:00:35.2178259+00:00 Microsoft.AspNetCore.Hosting.Diagnostics INF] Request starting HTTP/1.0 GET http://172.16.2.52/v1/user/test/account-balance
[2020-06-05T13:00:35.2181055+00:00 Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware DBG] The request path /v1/user/test/account-balance does not match a supported file type
[2020-06-05T13:00:36.2183025+00:00 Microsoft.AspNetCore.Server.Kestrel DBG] Connection id "0HM09A1MAAR26" started.
[2020-06-05T13:00:36.2195050+00:00 Microsoft.AspNetCore.Hosting.Diagnostics INF] Request starting HTTP/1.0 GET http://172.16.2.52/v1/user/test/account-balance
[2020-06-05T13:00:36.2199702+00:00 Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware DBG] The request path /v1/user/test/account-balance does not match a supported file type
[2020-06-05T13:00:37.2373822+00:00 Microsoft.AspNetCore.Server.Kestrel DBG] Connection id "0HM09A1MAAR27" started.

引發的幾種後果

客戶端調用超時

經過了 30S 甚至更長時間后看到大量的數據庫連接被初始化,然後開始集中式返回。

此時可能對於客戶端調用來說這一批請求都是超時的,

嚴重影響用戶體驗和某些依賴於此的其他接口。

數據庫連接暴漲

因為同時進入大量數據庫查詢請求觸發數據庫 DbContextPool 大量創建,

連接數隨之暴漲,數據庫查詢性能急速下降,可能引發其他的應用問題。

引發服務“雪崩”效應,服務不可用

請求堆積的情況下,

health-check 接口響應異常,

導致 k8s 主動重啟服務,重啟後繼續上述情況,

不斷惡化最後導致服務不可用。

排查問題

數據庫的問題 ?

當然,首先懷疑的就是數據庫了。

存在性能瓶頸?慢查詢導致不響應?發布期間存在其他的異常?

這類的問題都意義排查起來了。

最後發現,

這種情況發生的時候,數據庫監控裏面一片祥和。

數據庫 IO、CPU、內存都正常,

連接數暴漲是這種情況發生的時候帶來的,

而不是連接數暴漲之後導致了此情況。

數據庫驅動或者 EF Core 框架的問題?

是的,

這個懷疑一直都存在於腦海中。

最終,

昨天帶着“被挨罵的情況”去問了下“Pomelo.EntityFrameworkCore.MySql”的作者。


春天的熊 18:34:08
柚子啊,我這邊的.NET Core服務剛起來,建立MySQL連接的時候好慢,然後同一批請求可能無法正常響應,這個有什麼好的解決思路嗎?

Yuko丶柚子 18:34:29
Min Pool Size = 200

Yuko丶柚子 18:34:32
放連接字符串里

春天的熊 18:34:53
這個字段支持了嗎?

Yuko丶柚子 18:35:07
一直都支持

春天的熊 18:35:56
等等,      public static IServiceCollection AddDbContextPool<TContext>([NotNullAttribute] this IServiceCollection serviceCollection, [NotNullAttribute] Action<DbContextOptionsBuilder> optionsAction, int poolSize = 128) where TContext : DbContext;

春天的熊 18:36:13
這裏不是默認最大的128么?

Yuko丶柚子 18:36:18
你這個pool size是dbcontext的

Yuko丶柚子 18:36:21
我說的是mysql連接字符串的

Yuko丶柚子 18:36:28
dbcontext的pool有什麼用

春天的熊 18:43:13
我問個討打的問題,dbcontext 是具體的鏈接實例,EF用的,Min Pool Size 指的是這一個實例上面的連接池嗎“?

Yuko丶柚子 18:44:07
你在說什麼。。。

Yuko丶柚子 18:45:58
放到mysql的連接字符串上

Yuko丶柚子 18:46:14
這樣第一次調用MySqlConnection的時候就會建立200個連接

春天的熊 18:46:56
默認是多少來的?100嗎?

Yuko丶柚子 18:48:33
0

Yuko丶柚子 18:48:40
max默認是100

Yuko丶柚子 18:52:50
DbContextPool要解決的問題你都沒搞清楚

春天的熊 18:53:23
DbContextPool要解決的是盡量不去重複創建DbContext

Yuko丶柚子 18:53:34
為什麼不要重複創建DbContext

春天的熊 18:53:50
因為每個DbContext創建的代價很高,而且很慢

Yuko丶柚子 18:54:01
創建DbContext有什麼代價

Yuko丶柚子 18:54:03
哪裡慢了

Yuko丶柚子 18:54:06
都是毫秒級的

Yuko丶柚子 18:54:20
他的代價不在於創建 而在於回收

Yuko丶柚子 18:54:25
DbContextPool要解決的問題是 因為DbContext屬於較大的對象,而且是頻繁被new,而且經常失去引用導致GC頻繁工作。

Yuko 大大說的情況感覺會是一個思路,

所以第一反應就是加了參數控制連接池。

不過,無果。

5 個實例,

有 3 個實例正常啟動,

2 個實例會重複“雪崩”效應,最終無法正常啟動。

這個嘗試操作重複了好多次,

根據文檔和 Yuko 大大指導繼續加了不少 MySQL 鏈接參數,

最後,

重新學習了一波鏈接參數的優化意義,

無果。

究竟數據庫驅動有沒有問題?

沒什麼好的思路了,

遠程到容器裏面 Debug 基本不太現實(重新打包 + 容器化打包 + k8s + 人肉和服務器垮大洋),

要不,試試把日誌登錄調節到 Debug 看看所有的行為?

{
  "Using": ["Serilog.Sinks.Console"],
  "MinimumLevel": {
    "Default": "Debug",
    "Override": {
      "Microsoft": "Debug"
    }
  },
  "WriteTo": [
    {
      "Name": "Console",
      "Args": {
        "outputTemplate": "[{Timestamp:o}  {SourceContext} {Level:u3}] {Message:lj}{NewLine}{Exception}"
      }
    }
  ]
}

當然,這個事情沒有直接在正常的生產環境執行。

這裡是使用新配置,重新起新實例來操作。

然後我們看到程序啟動的時候執行 EFMigration 的時候,

程序和整個數據庫交互的完整日誌。


[2020-06-05T12:59:56.4147202+00:00 Microsoft.EntityFrameworkCore.Database.Connection DBG] Opening connection to database 'user_pool' on server 'aliyun-rds'.
[2020-06-05T12:59:56.4159970+00:00 Microsoft.EntityFrameworkCore.Database.Connection DBG] Opened connection to database 'user_pool' on server 'a'li'yun'.
[2020-06-05T12:59:56.4161172+00:00 Microsoft.EntityFrameworkCore.Database.Command DBG] Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='user_pool' AND TABLE_NAME='__EFMigrationsHistory';
[2020-06-05T12:59:56.4170776+00:00 Microsoft.EntityFrameworkCore.Database.Command INF] Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='user_pool' AND TABLE_NAME='__EFMigrationsHistory';
[2020-06-05T12:59:56.4171630+00:00 Microsoft.EntityFrameworkCore.Database.Connection DBG] Closing connection to database 'user_pool' on server 'aliyun-rds'.
[2020-06-05T12:59:56.4172458+00:00 Microsoft.EntityFrameworkCore.Database.Connection DBG] Closed connection to database 'user_pool' on server 'aliyun-rds'.
[2020-06-05T12:59:56.4385345+00:00 Microsoft.EntityFrameworkCore.Database.Command DBG] Creating DbCommand for 'ExecuteReader'.
[2020-06-05T12:59:56.4386201+00:00 Microsoft.EntityFrameworkCore.Database.Command DBG] Created DbCommand for 'ExecuteReader' (0ms).
[2020-06-05T12:59:56.4386763+00:00 Microsoft.EntityFrameworkCore.Database.Connection DBG] Opening connection to database 'user_pool' on server 'aliyun-rds'.
[2020-06-05T12:59:56.4400143+00:00 Microsoft.EntityFrameworkCore.Database.Connection DBG] Opened connection to database 'user_pool' on server 'aliyun-rds'.
[2020-06-05T12:59:56.4404529+00:00 Microsoft.EntityFrameworkCore.Database.Command DBG] Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT `MigrationId`, `ProductVersion`
FROM `__EFMigrationsHistory`
ORDER BY `MigrationId`;
[2020-06-05T12:59:56.4422387+00:00 Microsoft.EntityFrameworkCore.Database.Command INF] Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT `MigrationId`, `ProductVersion`
FROM `__EFMigrationsHistory`
ORDER BY `MigrationId`;
[2020-06-05T12:59:56.4446400+00:00 Microsoft.EntityFrameworkCore.Database.Command DBG] A data reader was disposed.
[2020-06-05T12:59:56.4447422+00:00 Microsoft.EntityFrameworkCore.Database.Connection DBG] Closing connection to database 'user_pool' on server 'aliyun-rds'.
[2020-06-05T12:59:56.4447975+00:00 Microsoft.EntityFrameworkCore.Database.Connection DBG] Closed connection to database 'user_pool' on server 'aliyun-rds'.
[2020-06-05T12:59:56.5170419+00:00 Microsoft.EntityFrameworkCore.Migrations INF] No migrations were applied. The database is already up to date.

看到這裏的時候,由於發現我們之前對 DbContext 和 DbConnection 的理解不太好,

想搞清楚究竟是不 db connection 創建的時候有哪些行為,

於是我們找到了 dotnet/efcore Github 的源碼開始拜讀,

PS: 源碼真香,能看源碼真好。

嘗試通過“Opening connection”找到日誌的場景。

想了解這個日誌輸出的時候代碼在做什麼樣的事情,可能同時會有哪些行為。

在考慮是不是其他的一些行為導致了上面的服務問題?

最終在RelationalConnection.cs確認上面這些數據庫相關日誌肯定是會輸出的,不存在其他的異常行為。

PS:不用細看,我們認真瀏覽了代碼之後確認 DbContext 正常初始化,

        /// <summary>
        ///     Asynchronously opens the connection to the database.
        /// </summary>
        /// <param name="errorsExpected"> Indicate if the connection errors are expected and should be logged as debug message. </param>
        /// <param name="cancellationToken">
        ///     A <see cref="CancellationToken" /> to observe while waiting for the task to complete.
        /// </param>
        /// <returns>
        ///     A task that represents the asynchronous operation, with a value of <see langword="true"/> if the connection
        ///     was actually opened.
        /// </returns>
        public virtual async Task<bool> OpenAsync(CancellationToken cancellationToken, bool errorsExpected = false)
        {
            if (DbConnection.State == ConnectionState.Broken)
            {
                await DbConnection.CloseAsync().ConfigureAwait(false);
            }

            var wasOpened = false;
            if (DbConnection.State != ConnectionState.Open)
            {
                if (CurrentTransaction != null)
                {
                    await CurrentTransaction.DisposeAsync().ConfigureAwait(false);
                }

                ClearTransactions(clearAmbient: false);
                await OpenDbConnectionAsync(errorsExpected, cancellationToken).ConfigureAwait(false);
                wasOpened = true;
            }

            _openedCount++;

            HandleAmbientTransactions();

            return wasOpened;
        }


        private async Task OpenDbConnectionAsync(bool errorsExpected, CancellationToken cancellationToken)
        {
            var startTime = DateTimeOffset.UtcNow;
            var stopwatch = Stopwatch.StartNew();

            // 日誌輸出在這裏
            var interceptionResult
                = await Dependencies.ConnectionLogger.ConnectionOpeningAsync(this, startTime, cancellationToken)
                    .ConfigureAwait(false);

            try
            {
                if (!interceptionResult.IsSuppressed)
                {
                    await DbConnection.OpenAsync(cancellationToken).ConfigureAwait(false);
                }
                // 日誌輸出在這裏
                await Dependencies.ConnectionLogger.ConnectionOpenedAsync(this, startTime, stopwatch.Elapsed, cancellationToken)
                    .ConfigureAwait(false);
            }
            catch (Exception e)
            {
                await Dependencies.ConnectionLogger.ConnectionErrorAsync(
                    this,
                    e,
                    startTime,
                    stopwatch.Elapsed,
                    errorsExpected,
                    cancellationToken)
                    .ConfigureAwait(false);

                throw;
            }

            if (_openedCount == 0)
            {
                _openedInternally = true;
            }
        }

當然,我們同時也去看了一眼 MySqlConnector的源碼,

確認它自身是維護了數據庫連接池的。到這裏基本確認不會是數據庫驅動導致的上述問題。

某種猜測

肯定是有什麼奇怪的行為阻塞了當前服務進程,

導致數據庫連接的日誌也沒有輸出。

鎖? 異步等同步?資源初始化問題?

周五晚上查到了這裏已經十一點了,

於是先下班回家休息了。

於是,

周六練完車之後 Call 了一下小夥伴,

又雙雙開始了愉快的 Debug。

插曲

小夥伴海林回公司前發了個朋友圈。

“ 咋們繼續昨天的 bug,

特此立 flag:修不好直播吃 bug

反正不是你死就是我亡…”

我調侃評論說:

你等下,我打包下代碼去樓下打印出來待會當晚飯

開始鎖定問題

中間件導致的嗎?

[2020-06-05T13:00:35.2181055+00:00 Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware DBG]

The request path /v1/user/test/account-balance does not match a supported file type

我們對着這個日誌思考了一會人生,

然後把引用此中間件的代碼註釋掉了,

不過,無果。

自定義 filters 導致的嗎?


[2020-06-05T13:01:05.3126001+00:00 Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker DBG] Execution plan of exception filters (in the following order): ["None"]
[2020-06-05T13:01:05.3126391+00:00 Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker DBG] Execution plan of result filters (in the following order): ["Microsoft.AspNetCore.Mvc.ViewFeatures.Filters.SaveTempDataFilter", "XXX.Filters.HTTPHeaderAttribute (Order: 0)"]
[2020-06-05T13:01:05.3072206+00:00 Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker DBG] Execution plan of authorization filters (in the following order): ["None"]

看到這個日誌我們考慮了一下,

是不是因為 filters 導致了問題。

畢竟在 HTTPHeaderAttribute 我們還還做了 ThreadLocal<Dictionary<string, string>> CurrentXHeaders

這裏懷疑是不是我們的實現存在鎖機制導致“假死問題”。

嘗試去掉。

不過,

無果。

嘗試使用 ptrace

沒什麼很好的頭緒了,要不上一下 ptrace 之類的工具跟一下系統調用?

最早在去年就嘗試過使用 ptrace 抓進程數據看系統調用,

後來升級到.NET Core3.0 之後,官方基於 Events + LTTng 之類的東西做了 dotnet-trace 工具,

官網說明:dotnet-trace performance analysis utility

改一下打包扔上去做一個數據收集看看。


FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build-env
WORKDIR /app

# copy csproj and restore as distinct layers
COPY src/*.csproj ./
RUN dotnet restore

COPY . ./

# copy everything else and build
RUN dotnet publish src -c Release -o /app/out


# build runtime image
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1

# debug
# Install .NET Core SDK
RUN dotnet_sdk_version=3.1.100 \
    && curl -SL --output dotnet.tar.gz https://dotnetcli.azureedge.net/dotnet/Sdk/$dotnet_sdk_version/dotnet-sdk-$dotnet_sdk_version-linux-x64.tar.gz \
    && dotnet_sha512='5217ae1441089a71103694be8dd5bb3437680f00e263ad28317665d819a92338a27466e7d7a2b1f6b74367dd314128db345fa8fff6e90d0c966dea7a9a43bd21' \
    && echo "$dotnet_sha512 dotnet.tar.gz" | sha512sum -c - \
    && rm -rf /usr/share/dotnet \
    && rm -rf /usr/bin/dotnet \
    && mkdir -p /usr/share/dotnet \
    && tar -ozxf dotnet.tar.gz -C /usr/share/dotnet \
    && rm dotnet.tar.gz \
    && ln -s /usr/share/dotnet/dotnet /usr/bin/dotnet \
    # Trigger first run experience by running arbitrary cmd
    && dotnet help
RUN dotnet tool install --global dotnet-trace
RUN dotnet tool install -g dotnet-dump
RUN dotnet tool install --global dotnet-counters
ENV PATH="$PATH:/root/.dotnet/tools"

# end debug

WORKDIR /app
COPY --from=build-env /app/out .
ENTRYPOINT ["dotnet", "Your-APP.dll"]


更新發布,等待服務正常啟動之後,

使用 ab -c 300 -n 3000 ‘http://172.16.2.52/v1/user/test/account-balance’ 模擬 300 個用戶同時請求,

使得程序進入上述的“假死狀態”。

接着立即進入容器,執行’dotnet-trace collect -p 1′ 開始收集日誌。

最後拿到了一份大概 13M trace.nettrace 數據, 這個文件是 PerView 支持的格式,

在 MacOS 或者 Linux 上無法使用。

好在 dotnet-trace convert 可以將 trace.nettrace 轉換成 speedscope/chromium 兩種格式。

speedscope/chromium

  • speedscope:A fast, interactive web-based viewer for performance profiles.

  • chrome-devtools evaluate-performance

  • 全新 Chrome Devtool Performance 使用指南

$dotnet-trace convert 20200606-1753-trace.nettrace.txt  --format Speedscope
$dotnet-trace convert 20200606-1753-trace.nettrace.txt --format chromium
$speedscope 20200606-1753-trace.nettrace.speedscope.json

然後,炸雞了。

  Downloads speedscope 20200606-1625.trace.speedscope.json
Error: Cannot create a string longer than 0x3fffffe7 characters
    at Object.slice (buffer.js:652:37)
    at Buffer.toString (buffer.js:800:14)
    at main (/home/liguobao/.nvm/versions/node/v12.16.2/lib/node_modules/speedscope/bin/cli.js:69:39)
    at processTicksAndRejections (internal/process/task_queues.js:97:5)

Usage: speedscope [filepath]

If invoked with no arguments, will open a local copy of speedscope in your default browser.
Once open, you can browse for a profile to import.

If - is used as the filepath, will read from stdin instead.

  cat /path/to/profile | speedscope -

哦, Buffer.toString 炸雞了。

看一眼 20200606-1625.trace.speedscope.json 多大?

900M。

牛逼。

那換 Chrome performance 看看。

手動裝載一下 20200606-1753-trace.nettrace.chromium.json 看看。

等下,20200606-1753-trace.nettrace.chromium.json 這貨多大?

哦,4G。應該沒事,Intel NUC 主機內存空閑 20G,吃掉 4G 完全是沒有問題的。

看着進度條加載,看着內存漲起來,

然後…Chrome 控制台奔潰。再見再見,原來大家彼此完全沒有信任了。

唉,再來一次,把文件控制在 5M 左右看看。

最後,把 20200606-1753-trace.nettrace.chromium.json 控制在 1.5G 了,

終於可以正常加載起來了。

Chrome Performance

首先, 我們看到監控裏面有一堆的線程

隨便選一個線程看看它做撒,選擇 Call Tree 之後 點點點點點。

從調用棧能看到 整個線程當前狀態是“PerformWaitCallback”

整個操作應該的開頭應該是

Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure.KestrelConnection.System.Threading.IThreadPoolWorkItem.Execute()

PS: Kestrel (https://github.com/aspnet/KestrelHttpServer) is a lightweight cross-platform web server that supports .NET Core and runs on multiple platforms such as Linux, macOS, and Windows. Kestrel is fully written in .Net core. It is based on libuv which is a multi-platform asynchronous eventing library.

PS 人話: .NET Core 內置的 HTTP Server,和 Spring Boot 中的 tomcat 組件類似

很正常,說明請求正常到了我們的服務裏面了。

再看一下剩下的調用鏈信息。

簡要的調用信息日誌在這裏:


System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start(!!0&)
Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure.KestrelConnection+<ExecuteAsync>d__32.MoveNext()
Microsoft.AspNetCore.Server.Kestrel.Core.Internal.HttpConnectionMiddleware`1[System.__Canon].OnConnectionAsync(class Microsoft.AspNetCore.Connections.ConnectionContext)   Microsoft.AspNetCore.Server.Kestrel.Core.Internal.HttpConnection.ProcessRequestsAsync(class Microsoft.AspNetCore.Hosting.Server.IHttpApplication`1<!!0>)
System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Start(!!0&)
System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start(!!0&)
Microsoft.AspNetCore.Server.Kestrel.Core.Internal.HttpConnection+<ProcessRequestsAsync>d__12`1[System.__Canon].MoveNext()
Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequestsAsync(class Microsoft.AspNetCore.Hosting.Server.IHttpApplication`1<!!0>)
System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Start(!!0&)
System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start(!!0&)
Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol+<ProcessRequestsAsync>d__216`1[System.__Canon].MoveNext()
Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests(class Microsoft.AspNetCore.Hosting.Server.IHttpApplication`1<!!0>)
System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Start(!!0&)
System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start(!!0&)
Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol+<ProcessRequests>d__217`1[System.__Canon].MoveNext()
Microsoft.AspNetCore.Hosting.HostingApplication.ProcessRequestAsync(class Context)
Microsoft.AspNetCore.HostFiltering.HostFilteringMiddleware.Invoke(class Microsoft.AspNetCore.Http.HttpContext)
UserCenter.ErrorHandlingMiddleware.Invoke(class Microsoft.AspNetCore.Http.HttpContext)
System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Start(!!0&)
System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start(!!0&)

......
......
......

e.StaticFiles.StaticFileMiddleware.Invoke(class Microsoft.AspNetCore.Http.HttpContext)
Microsoft.AspNetCore.Builder.UseMiddlewareExtensions+<>c__DisplayClass4_1.<UseMiddleware>b__2(class Microsoft.AspNetCore.Http.HttpContext)
dynamicClass.lambda_method(pMT: 00007FB6D3BBE570,class System.Object,pMT: 00007FB6D4739560,pMT: 00007FB6D0BF4F98)
Microsoft.AspNetCore.Builder.UseMiddlewareExtensions.GetService(class System.IServiceProvider,class System.Type,class System.Type)
Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(class System.Type)
System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Start(!!0&)

Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2[Microsoft.Extensions.DependencyInjection.ServiceLookup.RuntimeResolverContext,System.__Canon].VisitCallSite(class Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceCallSite,!0)
System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start(!!0&)
System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Start(!!0&)

看到這裏,其實又有了一些很給力的信息被暴露出來了。

PerformWaitCallback

  • 直接字面理解,線程正在執行等待回調

調用鏈信息

耐心點,把所有的電調用鏈都展開。

我們能看到程序已經依次經過了下面幾個流程:

->ProcessRequestsAsync(系統)

->ErrorHandlingMiddleware(已經加載了自定義的錯誤中間件)

-> HostFilteringMiddleware(加載了 Filter 中間件)

-> Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor(調用鏈中的最後一個操作)

對應最上面的日誌來說,

請求進來,經過了中間件和 Filter 都是沒問題的,

最後在 DependencyInjection(依賴注入) 中沒有了蹤跡。

到這裏,

再次驗證我們昨天的思路:

這是一個 “資源阻塞問題”產生的問題

雖然做 ptrace 是想能直接抓到“兇手”的,

最後發現並沒有能跟蹤到具體的實現,

那可咋辦呢?

控制變量實踐

已知:

  • 併發 300 訪問 /v1/user/test/account-balance 接口程序會假死
  • 移除 Filter 中間件不能解決問題
  • 併發 300 訪問 /v1/health 健康檢查接口程序正常
  • ptrace 信息告訴我們有“東西”在阻塞 DI 容器創建某些實例

開始控制變量 + 人肉二分查找。

挪接口代碼

/v1/user/test/account-balance 的邏輯是由 AccountService 實現的,

AccountService 大概依賴四五個其他的 Service 和 DBContext,

最主要的邏輯是加載用戶幾個賬號,然後計算一下 balance。

大概代碼如下:

/// <summary>
/// 獲取用戶的AccountBalance匯總信息
/// </summary>
public async Task<AccountBalanceStat> LoadAccountBalanceStatAsync(string owner)
{
    // 數據庫查詢
    var accounts = await _dbContext.BabelAccounts.Where(ac => ac.Owner == owner).ToListAsync();
    // 內存計算
    return ConvertToAccountBalanceStat(accounts);
}

什麼都不改,直接把代碼 CP 到 Health 接口測一下。

神奇,300 併發抗住了。

結論:

  • 上面這一段代碼並不會導致服務僵死
  • 數據庫驅動沒有問題,DbContext 沒有問題,數據庫資源使用沒有問題
  • 當前並不會觸發 DI 容器異常, 問題出在 /v1/user/test/account-balance 初始化

account-balance 有什麼神奇的東西嗎?


/// <summary>
/// 查詢用戶的Brick賬號餘額
/// </summary>
[HttpGet("v1/user/{owner}/account-balance")]
[SwaggerResponse(200, "獲取成功", typeof(AccountBrickStat))]
public async Task<IActionResult> GetAccountBricks(
    [FromRoute, SwaggerParameter("所有者", Required = true)] string owner)
{
    owner = await _userService.FindOwnerAsync(owner);
    return Ok(new { data = await _accountService.LoadAccountAsync(owner), code = 0 });
}

我們剛剛驗證了 LoadAccountAsync 的代碼是沒有問題的,

要不 UserService DI 有問題,要不 AccountService DI 有問題。

把 UserService 加入到 HealthController 中。


public HealthController(UserService userService, UserPoolDataContext dbContext)
{
    _dbContext = dbContext;
    _userService= userService;
}

Bool。

300 併發沒有撐住,程序僵死啦。

完美,

問題應該在 UserService DI 初始化了。

接下來就是一個個驗證 UserService DI 需要的資源,

EmailSDK 沒有問題,

HTTPHeaderTools 沒有問題,

UserActivityLogService 沒有問題。

RedisClient…
RedisClient…
RedisClient…

OK
OK
Ok

復現炸雞了。

原來是 Redis 的鍋?

是,

也不是。

先看下我們 RedisClient 是怎麼使用的。

// startup.cs 注入了單例的ConnectionMultiplexer
// 程序啟動的時候會調用InitRedis
private void InitRedis(IServiceCollection services)
{
    services.AddSingleton<ConnectionMultiplexer, ConnectionMultiplexer>(factory =>
    {
        ConfigurationOptions options = ConfigurationOptions.Parse(Configuration["RedisConnectionString"]);
        options.SyncTimeout = 10 * 10000;
        return ConnectionMultiplexer.Connect(options);
    });
}

//RedisClient.cs 通過構造函數傳入

public class RedisClient
{
    private readonly ConnectionMultiplexer _redisMultiplexer;

    private readonly ILogger<RedisClient> _logger;

    public RedisClient(ConnectionMultiplexer redisMultiplexer, ILogger<RedisService> logger)
    {
        _redisMultiplexer = redisMultiplexer;
        _logger = logger;
    }
}

DI 初始化 RedisClient 實例的時候,

需要執行 ConnectionMultiplexer.Connect 方法,

ConnectionMultiplexer.Connect 是同步阻塞的。

ConnectionMultiplexer.Connect 是同步阻塞的。

ConnectionMultiplexer.Connect 是同步阻塞的。

一切都能解釋了。

怎麼改?

// InitRedis 直接把鏈接創建好,然後直接注入到IServiceCollection中
private void InitRedis(IServiceCollection services)
{
    ConfigurationOptions options = ConfigurationOptions.Parse(Configuration["RedisConnectionString"]);
    options.SyncTimeout = 10 * 10000;
    var redisConnectionMultiplexer = ConnectionMultiplexer.Connect(options);
    services.AddSingleton(redisConnectionMultiplexer);
    Log.Information("InitRedis success.");
}

發布驗證,

開門放併發 300 + 3000 請求。

完美抗住,絲一般順滑。

還有更優的寫法嗎?

  • 看了下微軟 Cache 中間件源碼,更好的做法應該是通過信號量+異步鎖來創建 Redis 鏈接,下來再研究一下
  • 數據庫中可能也存在類似的問題,不過當前會在 Startup 中戳一下數據庫連接,應該問題不大。

復盤

程序啟動的時候依賴注入容器同步初始化 Redis 可能很慢(幾秒甚至更長)的時候,

其他的資源都在同步等待它初始化好,

最終導致請求堆積,引起程序雪崩效應。

Redis 初始化過慢並不每次都發生, 所以之前服務也只是偶發。

DI 初始化 Redis 連接的時候,redis 打來連接還是個同步的方法,

這種情況下還可能發生異步請求中等待同步資源產生阻塞問題。

同時還需要排查使用其他外部資源的時候是否會觸發同類問題。

幾個通用的小技巧

  • ptrace 對此類問題分析很有意義,不同語言框架都有類似的實現

  • 同步、異步概念的原理和實現都要了解,這個有利於理解一些奇奇怪怪的問題

  • 火焰圖、Chrome dev Performance 、speedscope 都是好東西

  • Debug 日誌能給更多的信息,在隔離生產的情況下大膽使用

  • 這輩子都不可能看源碼的,寫寫 CURD 多美麗?源碼真香,源碼真牛逼。

  • 控制變量驗證,大膽假設,小心求證,人肉二分查,先懷疑自己再懷疑框架

  • 搞事的時候不要自己一個人,有 Bug 一定要拉上小夥伴一起吃

相關資料

  • IBM Developer ptrace 嵌入式系統中進程間通信的監視方法

  • 分析進程調用 pstack 和 starce

  • pstack 显示每個進程的棧跟蹤

  • 微軟:dotnet-trace performance analysis utility

  • 知乎:全新 Chrome Devtool Performance 使用指南

  • speedscope A fast, interactive web-based viewer for performance profiles.

  • jdk 工具之 jstack(Java Stack Trace)

  • 阮一峰:如何讀懂火焰圖?

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

【其他文章推薦】

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

※超省錢租車方案

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

※推薦台中搬家公司優質服務,可到府估價

記一次Docker中Redis連接暴增的問題排查

周六生產服務器出現redis服務器不可用狀態,錯誤信息為:

狀態不可用,等待後台檢查程序恢復方可使用。Unexpected end of stream; expected type ‘Status’

如下圖所示,下圖6300就是我們redis服務器運行的端口。

 

頭一次碰到此類問題,心想難道是redis掛掉了,隨即通過telnet ip+端口。發現運行正常,然後就想着進入redis看下目前連接情況。一看發現竟然高達1903條這麼多。

然後想着應該是代碼創建redis連接過多導致的,查看代碼。

發現redis創建只有這一個地方有,這裏也是服務註冊時才執行。也就是應用程序啟動時才被執行一次。然後整個項目查找,沒有其他地方再有調用redis初始化。

心有不甘,難道是每次在redis讀寫數據時都會創建連接嗎?會和讀寫頻繁有關係嗎?總感覺不會啊,隨即創建測試代碼進行測試一番。

在本地搭建了一個redis環境,測試之前先看看接數多少,目前看只有1個,也就是目前的cmd連接客戶端,這個屬於正常的了。

開始測試,運行程序。代碼是創建一個連接對象,並一共測試1000次寫,和1000次讀。

 

不管我怎麼測試連接都是6個,那麼也就是說我們程序最多創建了5個連接,當然主要有線程池在裏面。

 

所以基本的存儲讀取這塊代碼肯定是沒問題。

但代碼這塊也沒算完全放棄排查,因為生產服務器通過docker運行着大約6個應用程序。都是連接的同一個redis,會不會是其他應用程序導致的?

然後就想直接通過redis 連接列表裡的中隨便一個端口來查詢對應的進程信息就可以知道是哪些應用程序了。

Linux 中通過查詢網絡端口號显示進程信息。

netstat -atunlp | grep 60852 

首先看這端口對應的IP,比如這裏第一個是172.17.0.1。熟悉docker的同學應該知道這個ip是docker網關IP。我們容器中的程序都是通過這個網關IP來和我們宿主主機來通訊的。我們通過ifconfig就能發現docker這個網關IP,第二個172.17.0.3:6379這個一看就是redis的容器IP,

這樣一看確實無法找到具體對應哪個容器中的程序和我們建立連接的。

有一個最笨的辦法就是挨個進入容器裏面。即docker exec –it test /bin/bash 然後查看當前容器的網絡連接情況。這樣非常麻煩,並且需要安裝很多組件才能執行一系列命令。

另外一個辦法lsof命令,如果沒有則需要安裝。我們可以通過進程去找所有網絡連接情況。

比如我們剛發現我們的進程主要是docker,他的pid是582251。

lsof -i |grep 582251或者 lsof -i -p 582251

結果如下圖,右邊其實出現了具體IP,這個IP就是docker容器具體的IP地址。

 

現在知道所有IP和端口了,我們將命令執行結果下載下來。

首先找到自己每個容器對應的IP。

docker inspect name |grep IPAddress    //name 容器名稱或者id

 

找到每個ip后然後根據剛下載的所有網絡連接信息進行統計,看哪個IP連接最多,最多的一個肯定有問題。

然後我就找到這個IP對應的容器部署的程序,然後看redis配置。發現線程池設為200。

另外我通過github,發現CSRedisCore還有個預熱機制,也就是preheat,他默認值就是5個預熱連接。

我們線程池設置的是200加上本身有個預熱機制5個連接,我不知道是不是會創建200*5=1000個。這個有時間再好好研究下源代碼,目前只是猜測。

我現在已經將redis修改為poolsize=5, preheat=false。線程池5個,並且關閉預熱機制。

 

修改我們連接配置,並重啟應用服務器和redis服務器(為了徹底清除已建立的連接)后發現連接數有減少,但沒有很多。後來查詢發現,是redis的idle空閑時長太長,導致連接池維持太多連接,沒有被釋放。

我們設置下超時為30s

執行CONFIG SET timeout 30 (單位是秒,此種方式只是臨時修改,針對當前運行有效。長效記得修改redis配置文件)

然後再看下連接數多少,這樣一下子就減少了很多。

總結:

1、 redis連接暴增,首先從自身應用程序出發去尋找問題,比如我這邊發現的連接池設置過大,加上默認的預熱機制等。還有盡可能的看代碼層面在創建連接是否會被多次觸發,如果有就必須要改正。現在都是通過注入的方式創建實例,要看該地方是存在被多次調用。

2、修改redis服務器配置,比如連接空閑超時時間。包括也可也看下最大連接數多少,默認值。

 

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

【其他文章推薦】

※回頭車貨運收費標準

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※推薦評價好的iphone維修中心

※教你寫出一流的銷售文案?

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

給非目標魚類的「逃生指示燈」 研究:漁網裝LED燈 可使混獲減半

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

※超省錢租車方案

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

※推薦台中搬家公司優質服務,可到府估價

蘇門答臘高速公路通車前夕驚見虎蹤 生態廊道功效恐不足 專家建議另增柵欄

環境資訊中心綜合外電;范震華 翻譯;賴慧玲 審校;稿源:Mongabay

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

※超省錢租車方案

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

※推薦台中搬家公司優質服務,可到府估價

毀林、武肺、林火三重危機 專家警告:亞馬遜今年火災恐比去年惡化50%

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※回頭車貨運收費標準

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※推薦評價好的iphone維修中心

※教你寫出一流的銷售文案?

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

全球屠宰廠深陷武肺危機 工人被迫復工染疫 百萬牲畜安樂死

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

網頁設計最專業,超強功能平台可客製化

※別再煩惱如何寫文案,掌握八大原則!

澳彩虹鎮BOM雷達站 助農民掌握天氣變化

摘錄自2020年5月15日公視報導

澳洲農業重鎮維多利亞省的西部小鎮「彩虹」,日前正式啟用聯邦氣象局設置的農業雷達站,讓當地農民可以更快獲得更準確的氣象資訊。

遠遠看去就像是一顆大型的高爾夫球放在發球點上,它是澳洲聯邦氣象局BOM,在全國設立的幾十座農業氣象雷達站中,最新啟用的一個。在這個球體的內部,有個全天候360度旋轉的碟型天線,是這款C波段雷達的靈魂。

由於利用都卜勒效應,雷達除了能判斷雲雨帶的強度與位置,還可以算出移動的方向跟速度,換言之,雷達站周邊的農場何時會受到雲雨帶影響,以及影響持續的時間,都可以算出來。聯邦氣象局的雷達站網路,成為農民們拿捏農作時間點最重要的依靠。

生活環境
全球變遷
氣候變遷
國際新聞
澳洲
雷達站
氣象

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

破紀錄 馬來西亞查獲6公噸穿山甲鱗片 偽裝腰果闖海關

環境資訊中心綜合外電;黃鈺婷 翻譯;林大利 審校;稿源:Mongabay

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※推薦台中搬家公司優質服務,可到府估價