漢娜颶風肆虐德州 吹倒美墨邊境圍牆

摘錄自2020年7月27日東森國際新聞美國報導

美國德州(Texas)新冠肺炎疫情持續肆虐,周末又遭颶風漢娜(Hanna)侵襲。漢娜是今年第一個颶風,它已經摧毀許多船隻、淹沒街道、造成電力中斷、甚至吹倒一部分美墨邊境圍牆,災情相當嚴重。

據外媒《WDSU News》報導,國家颶風中心(National Hurricane Center)表示,目前颶風漢娜已降為熱帶低氣壓,以每小時超過50英里的風速橫越美墨邊境,並在德州南部和墨西哥東北部的部分地區降下了超過300毫米的豪雨。

州長格雷格·阿博特 (Greg Abbott)於週六(25日)在一場記者會中表示,「任何颶風都是一場巨大的挑戰,但這次的挑戰非常複雜,必且比以往更加嚴峻,因為這場颶風正在席捲著新冠肺炎的其中一個震央。」

【其他文章推薦】

無塵擦拭布各大品牌廠商販售比價網!

臭氧機推薦

貨梯使用安全與保養

※高效率洗滌塔活性碳設備有哪些?

※掌握產品行銷策略,帶你認識商品包裝設計基本要素

Oracle 11g RAC之HAIP相關問題總結

1 文檔概要

2 禁用/啟用HAIP

  • 2.1 禁用/啟用HAIP資源
  • 2.2 修改ASM資源的依賴關係

3 修改cluster_interconnects參數

  • 3.1 使用grid用戶修改ASM實例的cluster_interconnects參數
  • 3.2 使用oracle用戶修改DB實例的cluster_interconnects參數
  • 3.3 重啟所有實例或集群
  • 3.4 檢查cluster_interconnects參數

1 文檔概要

環境:RHEL 6.4 + GI 11.2.0.4 + Oracle 11.2.0.4 對有關HAIP相關問題的總結,包括禁用/啟用HAIP,修改ASM資源的依賴關係,修改cluster_interconnects參數等。

2 禁用/啟用HAIP

2.1 禁用/啟用HAIP資源

禁用HAIP資源: root用戶執行@all nodes

# /opt/app/11.2.0/grid/bin/crsctl modify res ora.cluster_interconnect.haip -attr "ENABLED=0" -init

啟用HAIP資源:
如果之後想重新使用HAIP資源,可以啟用:

# /opt/app/11.2.0/grid/bin/crsctl modify res ora.cluster_interconnect.haip -attr "ENABLED=1" -init

在實際處理的案例中,發現其中一個節點始終無法啟動HAIP,在另外節點可以啟用HAIP,但這樣是無法啟動集群的,所以在可以啟動HAIP的節點直接禁用HAIP,這樣所有節點都使用真實的私網地址就可以正常啟動成功。但需要注意在有些場景下,單純禁用HAIP會導致ASM無法啟動,這是由於ASM資源的相關依賴關係導致。

2.2 修改ASM資源的依賴關係

在一些實際案例場景,我們直接禁用HAIP資源,再重啟has時可能會無法啟動ASM資源,因為ASM資源對HAIP有依賴關係。這一點我在自己的測試環境也可以驗證。

查看當前ASM資源的關聯關係:

[root@jyrac2 ~]# crsctl stat res ora.asm -p -init
NAME=ora.asm
TYPE=ora.asm.type
ACL=owner:grid:rw-,pgrp:oinstall:rw-,other::r--,user:grid:rwx
ACTION_FAILURE_TEMPLATE=
ACTION_SCRIPT=
ACTIVE_PLACEMENT=0
AGENT_FILENAME=%CRS_HOME%/bin/oraagent%CRS_EXE_SUFFIX%
AUTO_START=restore
CARDINALITY=1
CHECK_ARGS=
CHECK_COMMAND=
CHECK_INTERVAL=1
CHECK_TIMEOUT=30
CLEAN_ARGS=
CLEAN_COMMAND=
DAEMON_LOGGING_LEVELS=
DAEMON_TRACING_LEVELS=
DEFAULT_TEMPLATE=
DEGREE=1
DESCRIPTION="ASM instance"
DETACHED=true
ENABLED=1
FAILOVER_DELAY=0
FAILURE_INTERVAL=3
FAILURE_THRESHOLD=5
GEN_USR_ORA_INST_NAME=+ASM2
HOSTING_MEMBERS=
LOAD=1
LOGGING_LEVEL=1
NOT_RESTARTING_TEMPLATE=
OFFLINE_CHECK_INTERVAL=0
ORA_VERSION=11.2.0.4.0
PID_FILE=
PLACEMENT=balanced
PROCESS_TO_MONITOR=
PROFILE_CHANGE_TEMPLATE=
RESTART_ATTEMPTS=5
SCRIPT_TIMEOUT=600
SERVER_POOLS=
SPFILE=
START_ARGS=
START_COMMAND=
START_DEPENDENCIES=hard(ora.cssd,ora.cluster_interconnect.haip,ora.ctssd)pullup(ora.cssd,ora.cluster_interconnect.haip,ora.ctssd)weak(ora.drivers.acfs)
START_TIMEOUT=600
STATE_CHANGE_TEMPLATE=
STOP_ARGS=
STOP_COMMAND=
STOP_DEPENDENCIES=hard(intermediate:ora.cssd,shutdown:ora.cluster_interconnect.haip)
STOP_TIMEOUT=600
UNRESPONSIVE_TIMEOUT=180
UPTIME_THRESHOLD=1h
USR_ORA_ENV=
USR_ORA_INST_NAME=
USR_ORA_OPEN_MODE=mount
USR_ORA_OPI=false
USR_ORA_STOP_MODE=immediate
VERSION=11.2.0.3.0

可以看到ASM資源和HAIP資源的依賴關係。

修改ASM的關聯關係@all nodes:

crsctl modify resource ora.asm -attr "START_DEPENDENCIES='hard(ora.cssd,ora.ctssd)pullup(ora.cssd,ora.ctssd)weak(ora.drivers.acfs)'" -f -init 

crsctl modify resource ora.asm -attr "STOP_DEPENDENCIES=hard(intermediate:ora.cssd)" -f –init

改回ASM的關聯關係:

crsctl modify resource ora.asm -attr "START_DEPENDENCIES='hard(ora.cssd, ora.cluster_interconnect.haip, ora.ctssd)pullup(ora.cssd, ora.cluster_interconnect.haip, ora.ctssd)weak(ora.drivers.acfs)'" -f -init 

crsctl modify resource ora.asm -attr "STOP_DEPENDENCIES=hard(intermediate:ora.cssd)" -f –init 

3 修改cluster_interconnects參數

3.1 使用grid用戶修改ASM實例的cluster_interconnects參數

修改為具體的私網地址,示例如下:

SQL> alter system set cluster_interconnects='10.10.10.50' scope=spfile sid='+ASM1'; 
SQL> alter system set cluster_interconnects='10.10.10.52' scope=spfile sid='+ASM2';

改回默認值為空,示例如下:

SQL> alter system set cluster_interconnects='' scope=spfile sid='+ASM1'; 
SQL> alter system set cluster_interconnects='' scope=spfile sid='+ASM2';

3.2 使用oracle用戶修改DB實例的cluster_interconnects參數

修改為具體的私網地址,示例如下:

SQL> alter system set cluster_interconnects='10.10.10.50' scope=spfile sid='jyzhao1'; 
SQL> alter system set cluster_interconnects='10.10.10.52' scope=spfile sid='jyzhao2';

改回默認值為空,示例如下:

SQL> alter system set cluster_interconnects='' scope=spfile sid='jyzhao1'; 
SQL> alter system set cluster_interconnects='' scope=spfile sid='jyzhao2';

在實際的一個案例中,客戶是11g版本的GI環境,實際有2塊私網網卡,使用了HAIP特性,同時安裝有11g RAC和10g RAC,11g RAC使用HAIP正常,10g RAC由於無法使用HAIP,所以獲取到的是真實的私網地址,但是數據庫無法在所有節點同時open,這種情況,直接把10g RAC實例的cluster_interconnects參數修改成其中一個網卡的真實私網地址,即可正常在所有節點open。
這類場景是最適合修改參數解決,不影響其他正常使用HAIP的11g環境。

3.3 重啟所有實例或集群

修改參數之後需要重新啟動實例生效,這裏建議直接重啟集群一起驗證修改后的效果:

# /opt/app/11.2.0/grid/bin/crsctl stop has
# /opt/app/11.2.0/grid/bin/crsctl start has

3.4 檢查cluster_interconnects參數

SQL> show parameter cluster_interconnects

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

※回頭車貨運收費標準

設計模式系列之建造者模式(Builder Pattern)——複雜對象的組裝與創建

說明:設計模式系列文章是讀劉偉所著《設計模式的藝術之道(軟件開發人員內功修鍊之道)》一書的閱讀筆記。個人感覺這本書講的不錯,有興趣推薦讀一讀。詳細內容也可以看看此書作者的博客https://blog.csdn.net/LoveLion/article/details/17517213

模式概述

模式定義

沒有人買車會只買一個輪胎或者方向盤,大家買的都是一輛包含輪胎、方向盤和發動機等多個部件的完整汽車。如何將這些部件組裝成一輛完整的汽車並返回給用戶,這是建造者模式需要解決的問題。建造者模式又稱為生成器模式,它是一種較為複雜、使用頻率也相對較低的創建型模式。建造者模式為客戶端返回的不是一個簡單的產品,而是一個由多個部件組成的複雜產品。

它將客戶端與包含多個組成部分(或部件)的複雜對象的創建過程分離,客戶端無須知道複雜對象的內部組成部分與裝配方式,只需要知道所需建造者的類型即可。它關注如何一步一步創建一個的複雜對象,不同的具體建造者定義了不同的創建過程,且具體建造者相互獨立,增加新的建造者非常方便,無須修改已有代碼,系統具有較好的擴展性。

建造者模式(Builder Pattern):將一個複雜對象的構建與它的表示分離,使得同樣的構建過程可以創建不同的表示。建造者模式是一種對象創建型模式。

建造者模式一步一步創建一個複雜的對象,它允許用戶只通過指定複雜對象的類型和內容就可以構建它們,用戶不需要知道內部的具體構建細節。

模式結構圖

建造者模式結構圖如下所示:

建造者模式結構圖中包含如下幾個角色:

  • Builder(抽象建造者):它為創建一個產品Product對象的各個部件指定抽象接口,在該接口中一般聲明兩類方法,一類方法是buildPartX(),它們用於創建複雜對象的各個部件;另一類方法是getResult(),它們用於返回複雜對象。Builder既可以是抽象類,也可以是接口。

  • ConcreteBuilder(具體建造者):它實現了Builder接口,實現各個部件的具體構造和裝配方法,定義並明確它所創建的複雜對象,也可以提供一個方法返回創建好的複雜產品對象。

  • Product(產品角色):它是被構建的複雜對象,包含多個組成部件,具體建造者創建該產品的內部表示並定義它的裝配過程。

  • Director(指揮者):指揮者又稱為導演類,它負責安排複雜對象的建造次序,指揮者與抽象建造者之間存在關聯關係,可以在其construct()建造方法中調用建造者對象的部件構造與裝配方法,完成複雜對象的建造。客戶端一般只需要與指揮者進行交互,在客戶端確定具體建造者的類型,並實例化具體建造者對象(也可以通過配置文件和反射機制),然後通過指揮者類的構造函數或者Setter方法將該對象傳入指揮者類中。

在建造者模式的定義中提到了複雜對象,那麼什麼是複雜對象?簡單來說,複雜對象是指那些包含多個非簡單類型的成員屬性,這些成員屬性也稱為部件或零件,如汽車包括方向盤、發動機、輪胎等部件,电子郵件包括髮件人、收件人、主題、內容、附件等部件。

模式偽代碼

定義產品角色,典型代碼如下:

public class Product {
    // 定義部件,部件可以是任意類型,包括值類型和引用類型
    private String partA;

    private String partB;

    private String partC;

    // getter、setter方法省略
}

抽象建造者中定義了產品的創建方法和返回方法,其典型代碼如下

public abstract class Builder {
    // 創建產品對象
    protected Product product = new Product();

    public abstract void buildPartA();

    public abstract void buildPartB();

    public abstract void buildPartC();

    // 返回產品對象
    public Product getResult() {
        return product;
    }
}

在抽象類Builder中聲明了一系列抽象的buildPartX()方法用於創建複雜產品的各個部件,具體建造過程在ConcreteBuilder中實現,此外還提供了工廠方法getResult(),用於返回一個建造好的完整產品。

ConcreteBuilder中實現了buildPartX()方法,通過調用ProductsetPartX()方法可以給產品對象的成員屬性設值。不同的具體建造者在實現buildPartX()方法時將有所區別。

在建造者模式的結構中還引入了一個指揮者類Director,該類主要有兩個作用:一方面它隔離了客戶與創建過程;另一方面它控制產品的創建過程,包括某個buildPartX()方法是否被調用以及多個buildPartX()方法調用的先後次序等。指揮者針對抽象建造者編程,客戶端只需要知道具體建造者的類型,即可通過指揮者類調用建造者的相關方法,返回一個完整的產品對象。在實際生活中也存在類似指揮者一樣的角色,如一個客戶去購買電腦,電腦銷售人員相當於指揮者,只要客戶確定電腦的類型,電腦銷售人員可以通知電腦組裝人員給客戶組裝一台電腦。指揮者類的代碼示例如下:

public class Director {

    private Builder builder;

    public Director(Builder builder) {
        this.builder = builder;
    }

    public void setBuilder(Builder builder) {
        this.builder = builer;
    }

    //產品構建與組裝方法
    public Product construct() {

        builder.buildPartA();
        builder.buildPartB();
        builder.buildPartC();

        return builder.getResult();
    }
}

在指揮者類中可以注入一個抽象建造者類型的對象,其核心在於提供了一個建造方法construct(),在該方法中調用了builder對象的構造部件的方法,最後返回一個產品對象。

對於客戶端而言,只需關心具體的建造者即可,代碼片段如下所示:

public static void main(String[] args) {
    Builder builder = new ConcreteBuilder();

    Director director = new Director(builder);

    Product product = director.construct();
}

模式簡化

在有些情況下,為了簡化系統結構,可以將Director和抽象建造者Builder進行合併,在Builder中提供逐步構建複雜產品對象的construct()方法。

public abstract class Builder {
    // 創建產品對象
    protected Product product = new Product();

    public abstract void buildPartA();

    public abstract void buildPartB();

    public abstract void buildPartC();

    // 返回產品對象
    public Product construct() {
        buildPartA();
        buildPartB();
        buildPartC();
        return product;
    }
}

模式應用

模式在JDK中的應用

java.util.stream.Stream.Builder

public interface Builder<T> extends Consumer<T> {
    /**
     * Adds an element to the stream being built.
     */
    default Builder<T> add(T t) {
        accept(t);
        return this;
    }

    /**
     * Builds the stream, transitioning this builder to the built state
     */
    Stream<T> build();
}

模式在開源項目中的應用

看下Spring是如何構建org.springframework.web.servlet.mvc.method.RequestMappingInfo

/**
 * Defines a builder for creating a RequestMappingInfo.
 * @since 4.2
 */
public interface Builder {
  /**
   * Set the path patterns.
   */
  Builder paths(String... paths);

  /**
   * Set the request method conditions.
   */
  Builder methods(RequestMethod... methods);

  /**
   * Set the request param conditions.
   */
  Builder params(String... params);

  /**
   * Set the header conditions.
   * <p>By default this is not set.
   */
  Builder headers(String... headers);

  /**
   * Build the RequestMappingInfo.
   */
  RequestMappingInfo build();
}

Builder接口的默認實現,如下:

private static class DefaultBuilder implements Builder {

  private String[] paths = new String[0];

  private RequestMethod[] methods = new RequestMethod[0];

  private String[] params = new String[0];

  private String[] headers = new String[0];

  public DefaultBuilder(String... paths) {
    this.paths = paths;
  }

  @Override
  public Builder paths(String... paths) {
    this.paths = paths;
    return this;
  }

  @Override
  public DefaultBuilder methods(RequestMethod... methods) {
    this.methods = methods;
    return this;
  }

  @Override
  public DefaultBuilder params(String... params) {
    this.params = params;
    return this;
  }

  @Override
  public DefaultBuilder headers(String... headers) {
    this.headers = headers;
    return this;
  }
  
  @Override
  public RequestMappingInfo build() {
    ContentNegotiationManager manager = this.options.getContentNegotiationManager();

    PatternsRequestCondition patternsCondition = new PatternsRequestCondition(
        this.paths, this.options.getUrlPathHelper(), this.options.getPathMatcher(),
        this.options.useSuffixPatternMatch(), this.options.useTrailingSlashMatch(),
        this.options.getFileExtensions());

    return new RequestMappingInfo(this.mappingName, patternsCondition,
        new RequestMethodsRequestCondition(this.methods),
        new ParamsRequestCondition(this.params),
        new HeadersRequestCondition(this.headers),
        new ConsumesRequestCondition(this.consumes, this.headers),
        new ProducesRequestCondition(this.produces, this.headers, manager),
        this.customCondition);
  }
}

Spring框架中許多構建類的實例化使用了類似上面方式,總結有以下特點:

  1. Builder大多是構建類的內部類,構建類提供了一個靜態創建Builder的方法
  2. Builder返回構建類的實例,大多通過build()方法
  3. 構建過程有大量參數,除了幾個必要參數,用戶可根據自己所需選擇設置其他參數實例化對象

模式總結

建造者模式的核心在於如何一步步構建一個包含多個組成部件的完整對象,使用相同的構建過程構建不同的產品,在軟件開發中,如果我們需要創建複雜對象並希望系統具備很好的靈活性和可擴展性可以考慮使用建造者模式。

建造者模式抽象工廠模式有點相似,但是建造者模式返回一個完整的複雜產品,而抽象工廠模式返回一系列相關的產品;在抽象工廠模式中,客戶端通過選擇具體工廠來生成所需對象,而在建造者模式中,客戶端通過指定具體建造者類型並指導Director類如何去生成對象,側重於一步步構造一個複雜對象,然後將結果返回。如果將抽象工廠模式看成一個汽車配件生產廠,生成不同類型的汽車配件,那麼建造者模式就是一個汽車組裝廠,通過對配件進行組裝返回一輛完整的汽車。

主要優點

  1. 將產品本身與產品的創建過程解耦,使得相同的創建過程可以創建不同的產品對象
  2. 可以更加精細地控制產品的創建過程。將複雜產品的創建步驟分解在不同的方法中,使得創建過程更加清晰,也更方便使用程序來控制創建過程。

適用場景

(1) 需要生成的產品對象有複雜的內部結構,這些產品對象通常包含多個成員屬性。

(2) 需要生成的產品對象的屬性相互依賴,需要指定其生成順序。

(3) 隔離複雜對象的創建和使用,並使得相同的創建過程可以創建不同的產品。

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

【其他文章推薦】

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

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

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

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

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

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

Linux的文件系統及文件緩存知識點整理

https://www.luozhiyun.com/archives/291

Linux的文件系統

文件系統的特點

  1. 文件系統要有嚴格的組織形式,使得文件能夠以塊為單位進行存儲。

  2. 文件系統中也要有索引區,用來方便查找一個文件分成的多個塊都存放在了什麼位置。

  3. 如果文件系統中有的文件是熱點文件,近期經常被讀取和寫入,文件系統應該有緩存層。

  4. 文件應該用文件夾的形式組織起來,方便管理和查詢。

  5. Linux內核要在自己的內存裏面維護一套數據結構,來保存哪些文件被哪些進程打開和使用。

    總體來說,文件系統的主要功能梳理如下:

ext系列的文件系統的格式

inode與塊的存儲

硬盤分成相同大小的單元,我們稱為塊(Block)。一塊的大小是扇區大小的整數倍,默認是4K。在格式化的時候,這個值是可以設定的。

一大塊硬盤被分成了一個個小的塊,用來存放文件的數據部分。這樣一來,如果我們像存放一個文件,就不用給他分配一塊連續的空間了。我們可以分散成一個個小塊進行存放。這樣就靈活得多,也比較容易添加、刪除和插入數據。

inode就是文件索引的意思,我們每個文件都會對應一個inode;一個文件夾就是一個文件,也對應一個inode。

inode數據結構如下:

struct ext4_inode {
	__le16	i_mode;		/* File mode */
	__le16	i_uid;		/* Low 16 bits of Owner Uid */
	__le32	i_size_lo;	/* Size in bytes */
	__le32	i_atime;	/* Access time */
	__le32	i_ctime;	/* Inode Change time */
	__le32	i_mtime;	/* Modification time */
	__le32	i_dtime;	/* Deletion Time */
	__le16	i_gid;		/* Low 16 bits of Group Id */
	__le16	i_links_count;	/* Links count */
	__le32	i_blocks_lo;	/* Blocks count */
	__le32	i_flags;	/* File flags */
......
	__le32	i_block[EXT4_N_BLOCKS];/* Pointers to blocks */
	__le32	i_generation;	/* File version (for NFS) */
	__le32	i_file_acl_lo;	/* File ACL */
	__le32	i_size_high;
......
};

inode裏面有文件的讀寫權限i_mode,屬於哪個用戶i_uid,哪個組i_gid,大小是多少i_size_io,佔用多少個塊i_blocks_io,i_atime是access time,是最近一次訪問文件的時間;i_ctime是change time,是最近一次更改inode的時間;i_mtime是modify time,是最近一次更改文件的時間等。

所有的文件都是保存在i_block裏面。具體保存規則由EXT4_N_BLOCKS決定,EXT4_N_BLOCKS有如下的定義:

#define	EXT4_NDIR_BLOCKS		12
#define	EXT4_IND_BLOCK			EXT4_NDIR_BLOCKS
#define	EXT4_DIND_BLOCK			(EXT4_IND_BLOCK + 1)
#define	EXT4_TIND_BLOCK			(EXT4_DIND_BLOCK + 1)
#define	EXT4_N_BLOCKS			(EXT4_TIND_BLOCK + 1)

在ext2和ext3中,其中前12項直接保存了塊的位置,也就是說,我們可以通過i_block[0-11],直接得到保存文件內容的塊。

但是,如果一個文件比較大,12塊放不下。當我們用到i_block[12]的時候,就不能直接放數據塊的位置了,要不然i_block很快就會用完了。

那麼可以讓i_block[12]指向一個塊,這個塊裏面不放數據塊,而是放數據塊的位置,這個塊我們稱為間接塊。如果文件再大一些,i_block[13]會指向一個塊,我們可以用二次間接塊。二次間接塊裏面存放了間接塊的位置,間接塊裏面存放了數據塊的位置,數據塊裏面存放的是真正的數據。如果文件再大點,那麼i_block[14]同理。

這裏面有一個非常顯著的問題,對於大文件來講,我們要多次讀取硬盤才能找到相應的塊,這樣訪問速度就會比較慢。

為了解決這個問題,ext4做了一定的改變。它引入了一個新的概念,叫作Extents。比方說,一個文件大小為128M,如果使用4k大小的塊進行存儲,需要32k個塊。如果按照ext2或者ext3那樣散着放,數量太大了。但是Extents可以用於存放連續的塊,也就是說,我們可以把128M放在一個Extents裏面。這樣的話,對大文件的讀寫性能提高了,文件碎片也減少了。

Exents是一個樹狀結構:

每個節點都有一個頭,ext4_extent_header可以用來描述某個節點。

struct ext4_extent_header {
	__le16	eh_magic;	/* probably will support different formats */
	__le16	eh_entries;	/* number of valid entries */
	__le16	eh_max;		/* capacity of store in entries */
	__le16	eh_depth;	/* has tree real underlying blocks? */
	__le32	eh_generation;	/* generation of the tree */
};

eh_entries表示這個節點裏面有多少項。這裏的項分兩種,如果是恭弘=叶 恭弘子節點,這一項會直接指向硬盤上的連續塊的地址,我們稱為數據節點ext4_extent;如果是分支節點,這一項會指向下一層的分支節點或者恭弘=叶 恭弘子節點,我們稱為索引節點ext4_extent_idx。這兩種類型的項的大小都是12個byte。

/*
 * This is the extent on-disk structure.
 * It's used at the bottom of the tree.
 */
struct ext4_extent {
	__le32	ee_block;	/* first logical block extent covers */
	__le16	ee_len;		/* number of blocks covered by extent */
	__le16	ee_start_hi;	/* high 16 bits of physical block */
	__le32	ee_start_lo;	/* low 32 bits of physical block */
};
/*
 * This is index on-disk structure.
 * It's used at all the levels except the bottom.
 */
struct ext4_extent_idx {
	__le32	ei_block;	/* index covers logical blocks from 'block' */
	__le32	ei_leaf_lo;	/* pointer to the physical block of the next *
				 * level. leaf or next index could be there */
	__le16	ei_leaf_hi;	/* high 16 bits of physical block */
	__u16	ei_unused;
};

如果文件不大,inode裏面的i_block中,可以放得下一個ext4_extent_header和4項ext4_extent。所以這個時候,eh_depth為0,也即inode裏面的就是恭弘=叶 恭弘子節點,樹高度為0。

如果文件比較大,4個extent放不下,就要分裂成為一棵樹,eh_depth>0的節點就是索引節點,其中根節點深度最大,在inode中。最底層eh_depth=0的是恭弘=叶 恭弘子節點。

除了根節點,其他的節點都保存在一個塊4k裏面,4k扣除ext4_extent_header的12個byte,剩下的能夠放340項,每個extent最大能表示128MB的數據,340個extent會使你的表示的文件達到42.5GB。

inode位圖和塊位圖

inode的位圖大小為4k,每一位對應一個inode。如果是1,表示這個inode已經被用了;如果是0,則表示沒被用。block的位圖同理。

在Linux操作系統裏面,想要創建一個新文件,會調用open函數,並且參數會有O_CREAT。這表示當文件找不到的時候,我們就需要創建一個。那麼open函數的調用過程大致是:要打開一個文件,先要根據路徑找到文件夾。如果發現文件夾下面沒有這個文件,同時又設置了O_CREAT,就說明我們要在這個文件夾下面創建一個文件。

創建一個文件,那麼就需要創建一個inode,那麼就會從文件系統裏面讀取inode位圖,然後找到下一個為0的inode,就是空閑的inode。對於block位圖,在寫入文件的時候,也會有這個過程。

文件系統的格式

數據塊的位圖是放在一個塊裏面的,共4k。每位表示一個數據塊,共可以表示$4 * 1024 * 8 = 2{15}$個數據塊。如果每個數據塊也是按默認的4K,最大可以表示空間為$2{15} * 4 * 1024 = 2^{27}$個byte,也就是128M,那麼顯然是不夠的。

這個時候就需要用到塊組,數據結構為ext4_group_desc,這裏面對於一個塊組裡的inode位圖bg_inode_bitmap_lo、塊位圖bg_block_bitmap_lo、inode列表bg_inode_table_lo,都有相應的成員變量。

這樣一個個塊組,就基本構成了我們整個文件系統的結構。因為塊組有多個,塊組描述符也同樣組成一個列表,我們把這些稱為塊組描述符表。

我們還需要有一個數據結構,對整個文件系統的情況進行描述,這個就是超級塊ext4_super_block。裏面有整個文件系統一共有多少inode,s_inodes_count;一共有多少塊,s_blocks_count_lo,每個塊組有多少inode,s_inodes_per_group,每個塊組有多少塊,s_blocks_per_group等。這些都是這類的全局信息。

最終,整個文件系統格式就是下面這個樣子。

默認情況下,超級塊和塊組描述符表都有副本保存在每一個塊組裡面。防止這些數據丟失了,導致整個文件系統都打不開了。

由於如果每個塊組裡面都保存一份完整的塊組描述符表,一方面很浪費空間;另一個方面,由於一個塊組最大128M,而塊組描述符表裡面有多少項,這就限制了有多少個塊組,128M * 塊組的總數目是整個文件系統的大小,就被限制住了。

因此引入Meta Block Groups特性。

首先,塊組描述符表不會保存所有塊組的描述符了,而是將塊組分成多個組,我們稱為元塊組(Meta Block Group)。每個元塊組裡面的塊組描述符表僅僅包括自己的,一個元塊組包含64個塊組,這樣一個元塊組中的塊組描述符表最多64項。

我們假設一共有256個塊組,原來是一個整的塊組描述符表,裏面有256項,要備份就全備份,現在分成4個元塊組,每個元塊組裡面的塊組描述符表就只有64項了,這就小多了,而且四個元塊組自己備份自己的。

根據圖中,每一個元塊組包含64個塊組,塊組描述符表也是64項,備份三份,在元塊組的第一個,第二個和最後一個塊組的開始處。

如果開啟了sparse_super特性,超級塊和塊組描述符表的副本只會保存在塊組索引為0、3、5、7的整數冪里。所以上圖的超級塊只在索引為0、3、5、7等的整數冪里。

目錄的存儲格式

其實目錄本身也是個文件,也有inode。inode裏面也是指向一些塊。和普通文件不同的是,普通文件的塊裏面保存的是文件數據,而目錄文件的塊裏面保存的是目錄裏面一項一項的文件信息。這些信息我們稱為ext4_dir_entry。

在目錄文件的塊中,最簡單的保存格式是列表,每一項都會保存這個目錄的下一級的文件的文件名和對應的inode,通過這個inode,就能找到真正的文件。第一項是“.”,表示當前目錄,第二項是“…”,表示上一級目錄,接下來就是一項一項的文件名和inode。

如果在inode中設置EXT4_INDEX_FL標誌,那麼就表示根據索引查找文件。索引項會維護一個文件名的哈希值和數據塊的一個映射關係。

如果我們要查找一個目錄下面的文件名,可以通過名稱取哈希。如果哈希能夠匹配上,就說明這個文件的信息在相應的塊裏面。然後打開這個塊,如果裏面不再是索引,而是索引樹的恭弘=叶 恭弘子節點的話,那裡面還是ext4_dir_entry的列表,我們只要一項一項找文件名就行。通過索引樹,我們可以將一個目錄下面的N多的文件分散到很多的塊裏面,可以很快地進行查找。

Linux中的文件緩存

ext4文件系統層

對於ext4文件系統來講,內核定義了一個ext4_file_operations。

const struct file_operations ext4_file_operations = {
......
	.read_iter	= ext4_file_read_iter,
	.write_iter	= ext4_file_write_iter,
......
}

ext4_file_read_iter會調用generic_file_read_iter,ext4_file_write_iter會調用__generic_file_write_iter。

ssize_t
generic_file_read_iter(struct kiocb *iocb, struct iov_iter *iter)
{
......
    if (iocb->ki_flags & IOCB_DIRECT) {
......
        struct address_space *mapping = file->f_mapping;
......
        retval = mapping->a_ops->direct_IO(iocb, iter);
    }
......
    retval = generic_file_buffered_read(iocb, iter, retval);
}


ssize_t __generic_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
......
    if (iocb->ki_flags & IOCB_DIRECT) {
......
        written = generic_file_direct_write(iocb, from);
......
    } else {
......
		written = generic_perform_write(file, from, iocb->ki_pos);
......
    }
}

generic_file_read_iter和__generic_file_write_iter有相似的邏輯,就是要區分是否用緩存。因此,根據是否使用內存做緩存,我們可以把文件的I/O操作分為兩種類型。

第一種類型是緩存I/O。大多數文件系統的默認I/O操作都是緩存I/O。對於讀操作來講,操作系統會先檢查,內核的緩衝區有沒有需要的數據。如果已經緩存了,那就直接從緩存中返回;否則從磁盤中讀取,然後緩存在操作系統的緩存中。對於寫操作來講,操作系統會先將數據從用戶空間複製到內核空間的緩存中。這時對用戶程序來說,寫操作就已經完成。至於什麼時候再寫到磁盤中由操作系統決定,除非顯式地調用了sync同步命令。

第二種類型是直接IO,就是應用程序直接訪問磁盤數據,而不經過內核緩衝區,從而減少了在內核緩存和用戶程序之間數據複製。

如果在寫的邏輯__generic_file_write_iter裏面,發現設置了IOCB_DIRECT,則調用generic_file_direct_write,裏面同樣會調用address_space的direct_IO的函數,將數據直接寫入硬盤。

帶緩存的寫入操作

我們先來看帶緩存寫入的函數generic_perform_write。

ssize_t generic_perform_write(struct file *file,
				struct iov_iter *i, loff_t pos)
{
	struct address_space *mapping = file->f_mapping;
	const struct address_space_operations *a_ops = mapping->a_ops;
	do {
		struct page *page;
		unsigned long offset;	/* Offset into pagecache page */
		unsigned long bytes;	/* Bytes to write to page */
		status = a_ops->write_begin(file, mapping, pos, bytes, flags,
						&page, &fsdata);
		copied = iov_iter_copy_from_user_atomic(page, i, offset, bytes);
		flush_dcache_page(page);
		status = a_ops->write_end(file, mapping, pos, bytes, copied,
						page, fsdata);
		pos += copied;
		written += copied;


		balance_dirty_pages_ratelimited(mapping);
	} while (iov_iter_count(i));
}

循環中主要做了這幾件事:

  • 對於每一頁,先調用address_space的write_begin做一些準備;
  • 調用iov_iter_copy_from_user_atomic,將寫入的內容從用戶態拷貝到內核態的頁中;
  • 調用address_space的write_end完成寫操作;
  • 調用balance_dirty_pages_ratelimited,看臟頁是否太多,需要寫回硬盤。所謂臟頁,就是寫入到緩存,但是還沒有寫入到硬盤的頁面。

對於第一步,調用的是ext4_write_begin來說,主要做兩件事:

第一做日誌相關的工作。

ext4是一種日誌文件系統,是為了防止突然斷電的時候的數據丟失,引入了日誌(Journal)模式。日誌文件系統比非日誌文件系統多了一個Journal區域。文件在ext4中分兩部分存儲,一部分是文件的元數據,另一部分是數據。元數據和數據的操作日誌Journal也是分開管理的。你可以在掛載ext4的時候,選擇Journal模式。這種模式在將數據寫入文件系統前,必須等待元數據和數據的日誌已經落盤才能發揮作用。這樣性能比較差,但是最安全。

另一種模式是order模式。這個模式不記錄數據的日誌,只記錄元數據的日誌,但是在寫元數據的日誌前,必須先確保數據已經落盤。這個折中,是默認模式。

還有一種模式是writeback,不記錄數據的日誌,僅記錄元數據的日誌,並且不保證數據比元數據先落盤。這個性能最好,但是最不安全。

第二調用grab_cache_page_write_begin來,得到應該寫入的緩存頁。

struct page *grab_cache_page_write_begin(struct address_space *mapping,
					pgoff_t index, unsigned flags)
{
	struct page *page;
	int fgp_flags = FGP_LOCK|FGP_WRITE|FGP_CREAT;
	page = pagecache_get_page(mapping, index, fgp_flags,
			mapping_gfp_mask(mapping));
	if (page)
		wait_for_stable_page(page);
	return page;
}

在內核中,緩存以頁為單位放在內存裏面,每一個打開的文件都有一個struct file結構,每個struct file結構都有一個struct address_space用於關聯文件和內存,就是在這個結構裏面,有一棵樹,用於保存所有與這個文件相關的的緩存頁。

對於第二步,調用iov_iter_copy_from_user_atomic。先將分配好的頁面調用kmap_atomic映射到內核裏面的一個虛擬地址,然後將用戶態的數據拷貝到內核態的頁面的虛擬地址中,調用kunmap_atomic把內核裏面的映射刪除。

size_t iov_iter_copy_from_user_atomic(struct page *page,
		struct iov_iter *i, unsigned long offset, size_t bytes)
{
	char *kaddr = kmap_atomic(page), *p = kaddr + offset;
	iterate_all_kinds(i, bytes, v,
		copyin((p += v.iov_len) - v.iov_len, v.iov_base, v.iov_len),
		memcpy_from_page((p += v.bv_len) - v.bv_len, v.bv_page,
				 v.bv_offset, v.bv_len),
		memcpy((p += v.iov_len) - v.iov_len, v.iov_base, v.iov_len)
	)
	kunmap_atomic(kaddr);
	return bytes;
}

第三步中,調用ext4_write_end完成寫入。這裏面會調用ext4_journal_stop完成日誌的寫入,會調用block_write_end->__block_commit_write->mark_buffer_dirty,將修改過的緩存標記為臟頁。可以看出,其實所謂的完成寫入,並沒有真正寫入硬盤,僅僅是寫入緩存后,標記為臟頁

第四步,調用 balance_dirty_pages_ratelimited,是回寫臟頁。

/**
 * balance_dirty_pages_ratelimited - balance dirty memory state
 * @mapping: address_space which was dirtied
 *
 * Processes which are dirtying memory should call in here once for each page
 * which was newly dirtied.  The function will periodically check the system's
 * dirty state and will initiate writeback if needed.
  */
void balance_dirty_pages_ratelimited(struct address_space *mapping)
{
	struct inode *inode = mapping->host;
	struct backing_dev_info *bdi = inode_to_bdi(inode);
	struct bdi_writeback *wb = NULL;
	int ratelimit;
......
	if (unlikely(current->nr_dirtied >= ratelimit))
		balance_dirty_pages(mapping, wb, current->nr_dirtied);
......
}

在balance_dirty_pages_ratelimited裏面,發現臟頁的數目超過了規定的數目,就調用balance_dirty_pages->wb_start_background_writeback,啟動一個背後線程開始回寫。

另外還有幾種場景也會觸發回寫:

  • 用戶主動調用sync,將緩存刷到硬盤上去,最終會調用wakeup_flusher_threads,同步臟頁;
  • 當內存十分緊張,以至於無法分配頁面的時候,會調用free_more_memory,最終會調用wakeup_flusher_threads,釋放臟頁;
  • 臟頁已經更新了較長時間,時間上超過了設定時間,需要及時回寫,保持內存和磁盤上數據一致性。

帶緩存的讀操作

看帶緩存的讀,對應的是函數generic_file_buffered_read。

static ssize_t generic_file_buffered_read(struct kiocb *iocb,
		struct iov_iter *iter, ssize_t written)
{
	struct file *filp = iocb->ki_filp;
	struct address_space *mapping = filp->f_mapping;
	struct inode *inode = mapping->host;
	for (;;) {
		struct page *page;
		pgoff_t end_index;
		loff_t isize;
		page = find_get_page(mapping, index);
		if (!page) {
			if (iocb->ki_flags & IOCB_NOWAIT)
				goto would_block;
			page_cache_sync_readahead(mapping,
					ra, filp,
					index, last_index - index);
			page = find_get_page(mapping, index);
			if (unlikely(page == NULL))
				goto no_cached_page;
		}
		if (PageReadahead(page)) {
			page_cache_async_readahead(mapping,
					ra, filp, page,
					index, last_index - index);
		}
		/*
		 * Ok, we have the page, and it's up-to-date, so
		 * now we can copy it to user space...
		 */
		ret = copy_page_to_iter(page, offset, nr, iter);
    }
}

在generic_file_buffered_read函數中,我們需要先找到page cache裏面是否有緩存頁。如果沒有找到,不但讀取這一頁,還要進行預讀,這需要在page_cache_sync_readahead函數中實現。預讀完了以後,再試一把查找緩存頁。

如果第一次找緩存頁就找到了,我們還是要判斷,是不是應該繼續預讀;如果需要,就調用page_cache_async_readahead發起一個異步預讀。

最後,copy_page_to_iter會將內容從內核緩存頁拷貝到用戶內存空間。

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

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

解讀三組容易混淆的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/

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

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

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

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

起因和現象

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

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

所以 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維修中心

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

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

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

中國渤海發現億噸大油田 可供百萬台車開20年

摘錄自2020年5月27日聯合報報導

據中國網報導,中國海洋石油集團有限公司在位於渤海萊州灣北部地區的「墾利6-1油田」,探明的石油儲量超過1億噸,為中國渤海萊州灣北部地區第一個億噸級大型油田。

報導稱,「墾利6-1油田」隸屬中國海上最大油田渤海油田。自1970年代末開始,該地區歷經40餘年多輪勘探,效果均不理想,所發現的油田儲量規模小、分佈不集中,無法建立有效開發體系。

但近年來,渤海油田科研人員在勘探理論和地質認識上大膽創新,摸清了油氣藏的富集規律和勘探方向,在該地區斬獲億噸級探明地質儲量油田。按照原油常規採收率計算,「墾利6-1油田」開採出的原油提煉成汽柴油後,將可供100萬輛汽車行駛20餘年。

【其他文章推薦】

※廢氣洗滌塔,叫得動, 找得到的專業廠商‎

※市面十大品牌封口機!該如何選購?

塑膠射出成型加工商品有哪些?

貨梯使用安全與保養

※掌握產品行銷策略,帶你認識商品包裝設計基本要素

臭氧機推薦

氣候科學家在格陵蘭不幸喪命!「真實原因」曝光震撼全球

摘錄自2020年8月18日yahoo奇摩報導

日前一位世界著名的氣候學家史蒂芬(Konrad Steffen)在格陵蘭研究時,因為冰層破裂失足而逝世,享壽68歲。

綜合外媒報導,史蒂芬在1990年代就曾為了證明兩極冰川因為溫室效應導致嚴重的融化問題,與組織團隊在格陵蘭島徒步旅行,建造20個氣象站成為觀測網,此計畫至今都還在進行當中。

史蒂芬2007年提出警告,整個格陵蘭島「正在分崩離析」,更曾到美國國會聽證會上公開數據,「格陵蘭每年融化損失的冰量足以淹沒美國哥倫比亞特區」。

【其他文章推薦】

新北市探針選用參考標準?

※如何正確使用飲水機?

※攻戰消費者第一視覺,包裝設計很重要!

滑鼠墊適用各種文宣活動廣告曝光,專業客製服務

封口機購物網-不怕你比價,就怕你買貴!

關渡平原違建納管計畫啟動 延長期限免繳保證金

摘錄自2020年8月27日中央社報導

關渡平原違章建築嚴重,台北市政府原規劃拆除違建,引發討論。北市府都發局表示,修正後關渡平原輔導納管試辦計畫今天(27日)生效,包含業者可申請延長納管期限、免繳保證金等。

都發局發布新聞資料表示,經2次地區座談會聽取業者及民眾意見,修正關渡平原輔導納管試辦計畫,27日公告生效。

修正關渡平原輔導納管試辦計畫內容包含,違規業者可申請延長納管期限至今年底、納管期限延長至民國114年底、取消繳納保證金、農業及園藝業可免申請納管、增加公安評估替代方案、簡化自主管理計畫檢核表。

都發局統計今年初至8月26日,關渡平原違建及違規使用業者共444間,審查符合納管33間,其中9間已核准,24間待核准,另411間業者需依修正試辦計畫檢討內容再申請。呼籲符合資格業者在今年底前申請,逾期未申請,依規定查處。

【其他文章推薦】

無塵擦拭布各大品牌廠商販售比價網!

※如何正確使用飲水機?

※掌握產品行銷策略,帶你認識商品包裝設計基本要素

※精密CNC 自動車床設備介紹

空壓機這裡買最划算!