操屁眼的视频在线免费看,日本在线综合一区二区,久久在线观看免费视频,欧美日韩精品久久综

新聞資訊

    使用Redis過程中,可能會遇到各種問題。以下是一些常見問題及其解決方案:

    一、 緩存雪崩(Cache Avalanche)

    問題描述: Redis 雪崩(Redis Cache Avalanche)是指當(dāng)緩存中大量的緩存失效或者突然不可用時,大量請求直接打到后端數(shù)據(jù)庫,導(dǎo)致數(shù)據(jù)庫壓力劇增,甚至可能崩潰的現(xiàn)象。

    這個問題主要出現(xiàn)在高并發(fā)系統(tǒng)中,當(dāng)大量緩存數(shù)據(jù)在同一時間過期或者緩存服務(wù)器宕機(jī)時尤為嚴(yán)重。

    Redis 雪崩的常見原因:

    • 緩存集中過期:如果大量緩存數(shù)據(jù)設(shè)置了相同或接近的過期時間,當(dāng)這些緩存同時過期時,大量請求會同時打到后端數(shù)據(jù)庫。
    • 緩存服務(wù)器宕機(jī):當(dāng)緩存服務(wù)器宕機(jī)或不可用時,所有請求都會直接打到后端數(shù)據(jù)庫。
    • 熱點(diǎn)數(shù)據(jù)失效:熱門數(shù)據(jù)在緩存中失效,會導(dǎo)致大量請求直接打到數(shù)據(jù)庫,數(shù)據(jù)庫壓力驟增。

    解決方案:

    為了防止Redis緩存雪崩(Cache Avalanche)問題,可以設(shè)計(jì)一套多層次的防護(hù)方案架構(gòu)。

    以下是一個綜合性的方案架構(gòu),涵蓋了多種技術(shù)和策略來應(yīng)對可能的雪崩情況:

    緩存雪崩

    1. 緩存層設(shè)計(jì)

    1.1 隨機(jī)化過期時間

    設(shè)置緩存數(shù)據(jù)的過期時間時,加入一定的隨機(jī)性,避免大量緩存數(shù)據(jù)在同一時間過期。

    示例代碼:

    import random
    import redis
    
    cache_expire_time=3600  # 基礎(chǔ)過期時間,單位為秒
    random_expire_time=random.randint(0, 600)  # 隨機(jī)增加0到600秒的時間
    total_expire_time=cache_expire_time + random_expire_time
    
    redis_client.setex(key, total_expire_time, value)

    1.2 熱點(diǎn)數(shù)據(jù)永不過期

    對一些非常熱點(diǎn)的數(shù)據(jù),設(shè)置永不過期,并在后臺定時刷新這些數(shù)據(jù)。

    示例代碼:

    while True:
        # 獲取熱點(diǎn)數(shù)據(jù)
        value=fetch_hot_data_from_db()
        redis_client.set(key, value)  # 設(shè)置不過期的緩存
        time.sleep(refresh_interval)  # 定時刷新

    1.3 多級緩存

    在應(yīng)用層和分布式緩存之間增加本地緩存層,進(jìn)一步減少對后端數(shù)據(jù)庫的直接訪問。

    示例架構(gòu):

    [用戶請求] -> [本地緩存] -> [分布式緩存(Redis)] -> [后端數(shù)據(jù)庫]
    

    2. 請求層設(shè)計(jì)

    2.1 互斥鎖(Mutex)

    在緩存失效時,通過加鎖機(jī)制來控制只有一個線程去加載數(shù)據(jù)和更新緩存,其他線程等待鎖釋放后再訪問緩存。

    示例代碼:

    import threading
    
    lock=threading.Lock()
    
    def get_data(key):
        value=redis_client.get(key)
        if value is None:
            with lock:
                # 再次檢查緩存,防止多個線程進(jìn)入
                value=redis_client.get(key)
                if value is None:
                    # 加載數(shù)據(jù)并更新緩存
                    value=fetch_data_from_db()
                    redis_client.setex(key, expire_time, value)
        return value

    2.2 請求合并

    在緩存失效的瞬間,將多個對同一數(shù)據(jù)的請求合并為一個請求,減少對數(shù)據(jù)庫的訪問次數(shù)。

    示例代碼:

    pending_requests={}
    
    def get_data(key):
        if key in pending_requests:
            return pending_requests[key].result()
        else:
            future=concurrent.futures.Future()
            pending_requests[key]=future
            try:
                value=redis_client.get(key)
                if value is None:
                    value=fetch_data_from_db()
                    redis_client.setex(key, expire_time, value)
                future.set_result(value)
            finally:
                del pending_requests[key]
            return future.result()

    3. 數(shù)據(jù)層設(shè)計(jì)

    3.1 數(shù)據(jù)預(yù)熱

    在系統(tǒng)啟動或定期運(yùn)行時,將一部分常用或重要的數(shù)據(jù)預(yù)先加載到緩存中,避免首次請求未命中緩存。

    示例代碼:

    def cache_warming():
        hot_keys=get_hot_keys_from_db()
        for key in hot_keys:
            value=fetch_data_from_db(key)
            redis_client.setex(key, expire_time, value)
    
    # 在系統(tǒng)啟動時或定期調(diào)用
    cache_warming()

    3.2 限流降級

    在緩存失效的情況下,使用限流機(jī)制保護(hù)數(shù)據(jù)庫,或者降級處理,返回默認(rèn)值或提示用戶稍后重試。

    示例代碼:

    def get_data_with_rate_limit(key):
        if is_rate_limited():
            return get_default_value()
        try:
            value=redis_client.get(key)
            if value is None:
                value=fetch_data_from_db()
                redis_client.setex(key, expire_time, value)
            return value
        except Exception:
            return get_default_value()

    4. 運(yùn)維監(jiān)控

    4.1 監(jiān)控和報(bào)警

    對緩存命中率、數(shù)據(jù)庫訪問量等關(guān)鍵指標(biāo)進(jìn)行監(jiān)控,并設(shè)置報(bào)警閾值,及時發(fā)現(xiàn)和處理潛在問題。

    示例架構(gòu):

    • 使用Prometheus、Grafana等工具監(jiān)控緩存和數(shù)據(jù)庫的指標(biāo)。
    • 設(shè)置報(bào)警規(guī)則,當(dāng)緩存命中率低于一定值或數(shù)據(jù)庫訪問量超出預(yù)期時,觸發(fā)報(bào)警。

    二、 緩存擊穿(Cache Breakdown)

    問題描述: 緩存中的熱點(diǎn)數(shù)據(jù)在失效的瞬間,有大量請求并發(fā)地訪問該數(shù)據(jù),直接打到后端數(shù)據(jù)庫,造成數(shù)據(jù)庫壓力驟增。

    解決方案:

    • 熱點(diǎn)數(shù)據(jù)永不過期:對某些熱點(diǎn)數(shù)據(jù)設(shè)置永不過期,或者在接近過期時自動刷新緩存。同時啟動一個更新程序,對緩存數(shù)據(jù)定期更新。
    • 隨機(jī)化過期時間:或者設(shè)計(jì)過期時間隨機(jī),不要在同一時間大量蘇軒。

    三、 緩存穿透(Cache Penetration)

    問題描述: 查詢的數(shù)據(jù)既不在緩存中,也不在數(shù)據(jù)庫中,每次請求都會直接訪問數(shù)據(jù)庫,給數(shù)據(jù)庫帶來很大壓力。

    解決方案:

    為了防止Redis緩存穿透(Cache Penetration)問題,可以設(shè)計(jì)一個全面的解決方案架構(gòu)。以下是一個詳細(xì)的方案架構(gòu),涵蓋了多種技術(shù)和策略來應(yīng)對緩存穿透問題:

    緩存穿透

    1. 緩存層設(shè)計(jì)

    1.1 緩存空值

    對于查詢數(shù)據(jù)庫后確認(rèn)不存在的數(shù)據(jù),將空結(jié)果(如null或特定的占位符)緩存起來,設(shè)置一個短暫的過期時間,以避免頻繁查詢同一個不存在的數(shù)據(jù)。

    示例代碼:

    def get_data(key):
        value=redis_client.get(key)
        if value is None:
            value=fetch_data_from_db(key)
            if value is None:
                redis_client.setex(key, 60, "NULL")  # 緩存空值1分鐘
            else:
                redis_client.setex(key, 3600, value)  # 正常數(shù)據(jù)緩存1小時
        elif value=="NULL":
            return None
        return value

    1.2 布隆過濾器

    在緩存層前增加布隆過濾器,用于快速判斷一個數(shù)據(jù)是否存在,從而攔截對不存在數(shù)據(jù)的查詢請求。

    示例架構(gòu):

    from pybloom_live import BloomFilter
    
    # 初始化布隆過濾器
    bloom=BloomFilter(capacity=100000, error_rate=0.001)
    
    # 在數(shù)據(jù)寫入數(shù)據(jù)庫時更新布隆過濾器
    def add_to_bloom_filter(key):
        bloom.add(key)
    
    # 查詢時使用布隆過濾器
    def get_data(key):
        if key not in bloom:
            return None
        value=redis_client.get(key)
        if value is None:
            value=fetch_data_from_db(key)
            if value is None:
                redis_client.setex(key, 60, "NULL")
            else:
                redis_client.setex(key, 3600, value)
        elif value=="NULL":
            return None
        return value

    2. 請求層設(shè)計(jì)

    2.1 參數(shù)校驗(yàn)

    對請求的參數(shù)進(jìn)行合理性校驗(yàn),防止惡意請求進(jìn)入系統(tǒng)。

    示例代碼:

    def validate_request_params(params):
        if not params.get('key'):
            raise ValueError("Invalid parameters")
        # 其他參數(shù)校驗(yàn)邏輯
        return True
    
    def get_data(params):
        if validate_request_params(params):
            key=params['key']
            return fetch_data(key)
        return None

    2.2 限流策略

    對請求進(jìn)行限流,防止短時間內(nèi)大量惡意請求對系統(tǒng)造成沖擊。

    示例代碼:

    from ratelimit import limits, sleep_and_retry
    
    @sleep_and_retry
    @limits(calls=100, period=60)
    def fetch_data(key):
        # 數(shù)據(jù)獲取邏輯
        return get_data_from_cache_or_db(key)
    
    def get_data(key):
        try:
            return fetch_data(key)
        except Exception as e:
            return get_default_value()

    3. 數(shù)據(jù)層設(shè)計(jì)

    3.1 數(shù)據(jù)預(yù)熱

    在系統(tǒng)啟動或定期運(yùn)行時,將一部分常用或重要的數(shù)據(jù)預(yù)先加載到緩存中,避免首次請求未命中緩存。

    示例代碼:

    def cache_warming():
        hot_keys=get_hot_keys_from_db()
        for key in hot_keys:
            value=fetch_data_from_db(key)
            if value is not None:
                redis_client.setex(key, 3600, value)
    
    # 在系統(tǒng)啟動時或定期調(diào)用
    cache_warming()

    3.2 數(shù)據(jù)庫負(fù)載保護(hù)

    在緩存未命中的情況下,增加數(shù)據(jù)庫訪問的保護(hù)機(jī)制,防止數(shù)據(jù)庫被瞬時大量請求壓垮。

    示例代碼:

    import threading
    
    lock=threading.Lock()
    
    def get_data(key):
        value=redis_client.get(key)
        if value is None:
            with lock:
                # 再次檢查緩存,防止多個線程進(jìn)入
                value=redis_client.get(key)
                if value is None:
                    value=fetch_data_from_db(key)
                    if value is None:
                        redis_client.setex(key, 60, "NULL")
                    else:
                        redis_client.setex(key, 3600, value)
        elif value=="NULL":
            return None
        return value

    4. 運(yùn)維監(jiān)控

    4.1 監(jiān)控和報(bào)警

    對緩存命中率、數(shù)據(jù)庫訪問量等關(guān)鍵指標(biāo)進(jìn)行監(jiān)控,并設(shè)置報(bào)警閾值,及時發(fā)現(xiàn)和處理潛在問題。

    示例架構(gòu):

    • 使用Prometheus、Grafana等工具監(jiān)控緩存和數(shù)據(jù)庫的指標(biāo)。
    • 設(shè)置報(bào)警規(guī)則,當(dāng)緩存命中率低于一定值或數(shù)據(jù)庫訪問量超出預(yù)期時,觸發(fā)報(bào)警。

    通過以上多層次的防護(hù)方案架構(gòu),可以有效減少Redis緩存穿透的發(fā)生,提高系統(tǒng)的穩(wěn)定性和性能。

    四、 內(nèi)存不足

    問題描述: 當(dāng)Redis使用的內(nèi)存超過系統(tǒng)限制或配置的最大內(nèi)存時,可能導(dǎo)致Redis進(jìn)程被殺掉或者數(shù)據(jù)被淘汰。

    解決方案:

    • 合理設(shè)置最大內(nèi)存:通過配置maxmemory參數(shù)設(shè)置Redis可使用的最大內(nèi)存。
    • 淘汰策略:通過配置maxmemory-policy參數(shù)設(shè)置數(shù)據(jù)淘汰策略,如LRU(Least Recently Used)、LFU(Least Frequently Used)、TTL(Time to Live)等。
    • 數(shù)據(jù)壓縮:對存儲的數(shù)據(jù)進(jìn)行壓縮,減少內(nèi)存占用。
    • 定期清理:定期清理過期或不常用的數(shù)據(jù),釋放內(nèi)存。

    五、 數(shù)據(jù)持久化問題

    問題描述: 在使用RDB或AOF進(jìn)行數(shù)據(jù)持久化時,可能會遇到數(shù)據(jù)丟失或文件損壞等問題。

    解決方案:

    • 合理配置RDB和AOF:根據(jù)業(yè)務(wù)需求,合理配置RDB快照頻率和AOF同步策略,平衡性能和數(shù)據(jù)安全。
    • 多副本備份:定期備份RDB文件和AOF文件,并保存在不同的存儲介質(zhì)上,防止單點(diǎn)故障。
    • 數(shù)據(jù)恢復(fù):在數(shù)據(jù)文件損壞時,可以使用備份文件進(jìn)行數(shù)據(jù)恢復(fù)。

    六、 性能問題

    問題描述: 在高并發(fā)或大數(shù)據(jù)量場景下,Redis可能出現(xiàn)性能瓶頸。

    解決方案:

    • 水平擴(kuò)展:使用Redis Cluster進(jìn)行水平擴(kuò)展,將數(shù)據(jù)分布在多個節(jié)點(diǎn)上,提高并發(fā)處理能力。
    • Pipeline和批量操作:使用Pipeline和批量操作,減少網(wǎng)絡(luò)開銷,提高操作效率。
    • 合理使用數(shù)據(jù)結(jié)構(gòu):根據(jù)業(yè)務(wù)需求選擇合適的數(shù)據(jù)結(jié)構(gòu),避免使用復(fù)雜度較高的數(shù)據(jù)結(jié)構(gòu)和操作。
    • 監(jiān)控和優(yōu)化:使用監(jiān)控工具(如Redis Monitor、Prometheus等)監(jiān)控Redis性能,及時發(fā)現(xiàn)和解決性能瓶頸。

    七、數(shù)據(jù)一致性問題

    問題描述: 在主從復(fù)制或分布式環(huán)境下,可能會出現(xiàn)數(shù)據(jù)不一致的問題。

    解決方案:

    • 強(qiáng)一致性配置:在主從復(fù)制場景下,配置強(qiáng)一致性選項(xiàng),如wait命令確保寫操作被從節(jié)點(diǎn)確認(rèn)。
    • 數(shù)據(jù)同步監(jiān)控:使用監(jiān)控工具定期檢查主從數(shù)據(jù)一致性,及時發(fā)現(xiàn)并修復(fù)不一致問題。
    • 合理選擇一致性模型:根據(jù)業(yè)務(wù)需求選擇合適的一致性模型,權(quán)衡一致性、可用性和性能。

    Split lock 是 CPU 為了支持跨 cache line 進(jìn)行原子內(nèi)存訪問而支持的內(nèi)存總線鎖。

    有些處理器比如 ARM、RISC-V 不允許未對齊的內(nèi)存訪問,不會產(chǎn)生跨 cache line 的原子訪問,所以不會產(chǎn)生 split lock,而 X86 是支持的。

    split lock 對開發(fā)者來說是很方便的,因?yàn)椴恍枰紤]內(nèi)存不對齊訪問的問題,但是這同時也是有代價的:一個產(chǎn)生 split lock 的指令會獨(dú)占內(nèi)存總線大約 1000 個時鐘周期,對比正常情況下的 ADD 指令約只需要小于 10 個時鐘周期,鎖住內(nèi)存總線導(dǎo)致其他 CPU 無法訪問內(nèi)存會嚴(yán)重影響系統(tǒng)性能。

    因此 split lock 的檢測與處理就非常重要,現(xiàn)在的 CPU 支持檢測能力,檢測到如果在內(nèi)核態(tài)會直接 panic,在用戶態(tài)則會嘗試主動 sleep 來降低 split lock 產(chǎn)生的頻率,或者 kill 用戶態(tài)進(jìn)程,進(jìn)而緩解對內(nèi)存總線的爭搶。

    在引入了虛擬化后,會嘗試在 Host 側(cè)處理,KVM 通知 QEMU 的 vCPU 線程主動 sleep 降低 split lock 產(chǎn)生的頻率,甚至 kill 虛擬機(jī)。以上的結(jié)論也只是截止目前 2022/4/19(下同)的情況,近 2 年社區(qū)仍對 split lock 的處理有不同的看法,處理方式也是改變了多次,所以以下的分析僅討論目前的情況。

    1. Split lock 背景

    1.1 從 i++說起

    我們假設(shè)一個最簡單的計(jì)算模型,一個 CPU(單核、沒有開啟 Hyper-threading、沒有 Cache),一塊內(nèi)存。上面運(yùn)行一個 C 程序在執(zhí)行i++,對應(yīng)的匯編代碼是add 1, i

    分析一下這里add指令的語義,需要兩個操作數(shù),源操作數(shù) SRC 和目的操作數(shù) DEST,實(shí)現(xiàn)的功能是DEST=DEST + SRC。這里 SRC 是立即數(shù) 1,DEST 是 i 的內(nèi)存地址,CPU 需要先在內(nèi)存中讀出 i 的內(nèi)容,然后加 1,最后把結(jié)果寫入 i 所在的內(nèi)存地址。總共產(chǎn)生了兩次串行的內(nèi)存操作。

    如果計(jì)算架構(gòu)復(fù)雜一點(diǎn),有 2 個 CPU 核 CoreA 和 CoreB 的情況下,上面的i++代碼就不得不考慮數(shù)據(jù)一致性的問題:

    1.1.1 并發(fā)寫問題

    如果 CoreA 正在向 i 的內(nèi)存地址中寫入時,CoreB 同時向 i 的內(nèi)存地址寫入怎么辦?

    并發(fā)寫相同內(nèi)存地址其實(shí)很簡單,CPU 從硬件上保證了基礎(chǔ)內(nèi)存操作的原子性。

    具體的操作有:

    • 讀/寫 1 byte
    • 讀/寫 16 bit 對齊的 2 byte
    • 讀/寫 32 bit 對齊的 4 byte
    • 讀/寫 64 bit 對齊的 8 byte

    1.1.2 寫覆蓋問題

    如果 CoreA 從內(nèi)存中讀出 i 后,寫入 i 所在內(nèi)存地址前這段時間內(nèi),CoreB 向 i 的內(nèi)存地址寫入數(shù)據(jù)怎么辦?

    這種情況下會導(dǎo)致 CoreB 寫入的數(shù)據(jù)被 CoreA 后面再寫入的數(shù)據(jù)覆蓋掉,使 CoreB 的寫入數(shù)據(jù)丟失,而 CoreA 也不知道寫入的數(shù)據(jù)已經(jīng)在讀出后被更新過了。

    為什么會出現(xiàn)這個問題呢?就是因?yàn)?ADD 指令不是原子操作,會產(chǎn)生兩次內(nèi)存操作。

    那怎么解決這個問題呢?既然 ADD 指令在硬件上不是原子的,那么就從軟件上加鎖來實(shí)現(xiàn)原子操作,使 CoreB 的的內(nèi)存操作在 CoreA 的內(nèi)存操作完成前不能執(zhí)行。

    對應(yīng)方法就是聲明指令前綴LOCK,匯編代碼變?yōu)?span style="color: #35B378; --tt-darkmode-color: #35B378;">lock add 1, i

    1.2 總線鎖

    LOCK指令前綴聲明后,隨同執(zhí)行的指令會變?yōu)樵又噶睢T砭褪窃陔S同指令執(zhí)行期間,鎖住系統(tǒng)總線,禁止其他處理器進(jìn)行內(nèi)存操作,使其獨(dú)占內(nèi)存來實(shí)現(xiàn)原子操作。

    下面舉幾個例子:

    1.2.1 QEMU 中的原子累加

    QEMU 中的函數(shù) qatomic_inc(ptr),把參數(shù) ptr 指向的內(nèi)存數(shù)據(jù)進(jìn)行進(jìn)行加 1。

    #define qatomic_inc(ptr)        ((void) __sync_fetch_and_add(ptr, 1))
    

    原理是調(diào)用 GCC 內(nèi)置的__sync_fetch_and_add 函數(shù),我們手寫一個 C 程序,看下__sync_fetch_and_add 的匯編實(shí)現(xiàn)。

    int main() {
        int i=1;
        int *p=&i;
        while(1) {
            __sync_fetch_and_add(p, 1);
        }
        return 0;
    }
    
    // add.s
            .file   "add.c"
            .text
            .globl  main
            .type   main, @function
    main:
    .LFB0:
            .cfi_startproc
            pushq   %rbp
            .cfi_def_cfa_offset 16
            .cfi_offset 6, -16
            movq    %rsp, %rbp
            .cfi_def_cfa_register 6
            movl    $1, -12(%rbp)
            leaq    -12(%rbp), %rax
            movq    %rax, -8(%rbp)
    .L2:
            movq    -8(%rbp), %rax
            lock addl       $1, (%rax)
            jmp     .L2
            .cfi_endproc
    .LFE0:
            .size   main, .-main
            .ident  "GCC: (Debian 6.3.0-18+deb9u1) 6.3.0 20170516"
            .section        .note.GNU-stack,"",@progbits
    

    可以看到__sync_fetch_and_add 的匯編實(shí)現(xiàn)就是在 add 指令前聲明了 lock 指令前綴。

    1.2.2 Kernel 中的原子累加

    Kernel 中的 atomic_inc 函數(shù),把參數(shù) v 指向的內(nèi)存數(shù)據(jù)進(jìn)行進(jìn)行加 1。

    static __always_inline void
    atomic_inc(atomic_t *v)
    {
            instrument_atomic_read_write(v, sizeof(*v));
            arch_atomic_inc(v);
    }
    
    static __always_inline void arch_atomic_inc(atomic_t *v)
    {
            asm volatile(LOCK_PREFIX "incl %0"
                         : "+m" (v->counter) :: "memory");
    }
    
    #define LOCK_PREFIX LOCK_PREFIX_HERE "\n\tlock; "
    

    可以看到,同樣是聲明了 lock 指令前綴。

    1.2.3 CAS(Compare And Swap)

    編程語言中的 CAS 接口為開發(fā)者提供了原子操作,實(shí)現(xiàn)無鎖機(jī)制。

    Golang 的 CAS

    // bool Cas(int32 *val, int32 old, int32 new)
    // Atomically:
    //        if(*val==old){
    //                *val=new;
    //                return 1;
    //        } else
    //                return 0;
    TEXT ·Cas(SB),NOSPLIT,$0-17
            MOVQ        ptr+0(FP), BX
            MOVL        old+8(FP), AX
            MOVL        new+12(FP), CX
            LOCK
            CMPXCHGL        CX, 0(BX)
            SETEQ        ret+16(FP)
            RET
    

    Java 的 CAS

    inline jlong    Atomic::cmpxchg    (jlong    exchange_value, volatile jlong*    dest, jlong    compare_value) {
      bool mp=os::is_MP();
      __asm__ __volatile__ (LOCK_IF_MP(%4) "cmpxchgq %1,(%3)"
                            : "=a" (exchange_value)
                            : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                            : "cc", "memory");
      return exchange_value;
    }
    
    // Adding a lock prefix to an instruction on MP machine
    #define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "
    

    可以看到,CAS 同樣是使用 lock 指令前綴來實(shí)現(xiàn)的,那么 lock 指令前綴具體是怎么實(shí)現(xiàn)的呢?

    1.2.4 LOCK#信號

    具體來說,代碼中的指令前面聲明了 LOCK 前綴指令后,處理器就會在指令運(yùn)行期間產(chǎn)生 LOCK#信號,使其他處理器不能通過總線訪問內(nèi)存。

    我們嘗試從 8086 CPU 的引腳圖中管中窺豹,了解下 LOCK#信號的原理。

    8086 CPU 存在一個 LOCK 引腳(圖中 29 號引腳),低電平有效。當(dāng)聲明 LOCK 指令前綴時,會拉低 LOCK 引腳電平,進(jìn)行 assert 操作,此時其他設(shè)備無法獲取系統(tǒng)總線的控制權(quán)。當(dāng) LOCK 指令修飾的指令執(zhí)行完成后,拉高 LOCK 引腳電平進(jìn)行 de-assert。

    所以整個流程就清晰了,當(dāng)想要通過非原子指令(例如 add)實(shí)現(xiàn)原子操作時,編程時需要在指令前聲明 lock 指令前綴,運(yùn)行時 lock 指令前綴會被處理器識別出來,并產(chǎn)生 LOCK#信號,使其獨(dú)占內(nèi)存總線,而其他處理器則無法通過內(nèi)存總線訪問內(nèi)存,這樣就實(shí)現(xiàn)了原子操作。所以也就解決了上面的寫覆蓋問題了。

    看起來很好,不過這樣又引入了一個新問題:

    1.2.5 總線鎖引起的性能下降問題

    現(xiàn)在處理器的核越來越多,如果每個核都頻繁的產(chǎn)生 LOCK#信號,來獨(dú)占內(nèi)存總線,這樣其余的核不能訪問內(nèi)存,導(dǎo)致性能會有很大的下降,該怎么辦?

    1.3 緩存鎖

    INTEL 為了優(yōu)化總線鎖導(dǎo)致的性能問題,在 P6 后的處理器上,引入了緩存鎖(cache locking)機(jī)制:通過緩存一致性協(xié)議保證多個 CPU 核訪問跨 cache line 的內(nèi)存地址的多次訪問的原子性與一致性,而不需要鎖內(nèi)存總線。

    1.3.1 MESI 協(xié)議

    先以常見的 MESI 簡單介紹一下緩存一致性協(xié)議。MESI 分為四種狀態(tài):

    1. 已修改 Modified (M) 緩存行是臟的(dirty),與主存的值不同。如果別的 CPU 內(nèi)核要讀主存這塊數(shù)據(jù),該緩存行必須回寫到主存,狀態(tài)變?yōu)楣蚕?S).
    2. 獨(dú)占 Exclusive (E) 緩存行只在當(dāng)前緩存中,但是干凈的(clean)--緩存數(shù)據(jù)同于主存數(shù)據(jù)。當(dāng)別的緩存讀取它時,狀態(tài)變?yōu)楣蚕恚划?dāng)前寫數(shù)據(jù)時,變?yōu)橐研薷臓顟B(tài)。
    3. 共享 Shared (S) 緩存行也存在于其它緩存中且是干凈的。緩存行可以在任意時刻拋棄。
    4. 無效 Invalid (I) 緩存行是無效的

    MESI 協(xié)議狀態(tài)機(jī)如下:

    狀態(tài)機(jī)的轉(zhuǎn)換基于兩種情況:

    1. CPU 產(chǎn)生對 cache 的請求a. PrRd: CPU 請求讀一個緩存塊b. PrWr: CPU 請求寫一個緩存塊
    2. 總線產(chǎn)生對 cache 的請求a. BusRd: 窺探器請求指出其他處理器請求讀一個緩存塊b. BusRdX: 窺探器請求指出其他處理器請求寫一個該處理器不擁有的緩存塊c. BusUpgr: 窺探器請求指出其他處理器請求寫一個該處理器擁有的緩存塊d. Flush: 窺探器請求指出請求回寫整個緩存到主存e. FlushOpt: 窺探器請求指出整個緩存塊被發(fā)到總線以發(fā)送給另外一個處理器(緩存到緩存的復(fù)制)

    簡單來說,通過 MESI 協(xié)議,每個 CPU 不僅知道自身對 cache 的讀寫操作,還進(jìn)行總線嗅探(snooping),可以知道其他 CPU 對 cache 的的讀寫操作,所以除了自身對 cache 的修改也會根據(jù)其他 CPU 對 cache 的修改來改變 cache 的狀態(tài)。

    1.3.2 緩存鎖原理

    緩存鎖是依賴緩存一致性協(xié)議來保證內(nèi)存訪問的原子性,因?yàn)榫彺嬉恢滦詤f(xié)議會阻止被多個 CPU 緩存的內(nèi)存地址被多個 CPU 同時修改。

    下面我們以一個例子分析緩存鎖是如何基于 MESI 協(xié)議實(shí)現(xiàn)內(nèi)存讀寫的原子性。

    我們還是假設(shè)有兩個 CPU Core,CoreA 與 CoreB 進(jìn)行分析。

    注意最后一個操作步驟 4,CoreB 修改 cache 中的數(shù)據(jù)后,當(dāng) CoreA 想再次修改時,會被 CoreB 嗅探到,只有等 CoreB 的數(shù)據(jù)同步到主存與 CoreA 后,CoreA 才會進(jìn)行修改。

    可以看到 CoreB 修改的數(shù)據(jù)沒有丟失,被同步給了 CoreA 與主存。并且實(shí)現(xiàn)上述的操作沒有鎖內(nèi)存總線,只是 CoreA 的修改操作被堵塞了一下,這相比鎖整個內(nèi)存總線是可控的。

    上面是一個比較簡單的情況,兩個 CPU Core 的寫入是串行的。那么如果在操作步驟 2 后,CoreA 與 CoreB 同時下發(fā)寫請求呢?會產(chǎn)生兩個 Core 的 cache 都進(jìn)入 M 狀態(tài)嗎?

    答案是否定的,MESI 協(xié)議保證了上面同時進(jìn)入 M 的情況不會發(fā)生。根據(jù) MESI 協(xié)議,一個 Core 的 PrWr 操作只能在其 cache 為 M 或 E 狀態(tài)時自由的執(zhí)行,如果是 S 狀態(tài),其他 Core 的 cache 必須先被設(shè)置為 I 狀態(tài),實(shí)現(xiàn)的方式是通過一個叫 Request For Ownership(RFO)的總線廣播進(jìn)行的,RFO 是一個總線事務(wù),如果兩個 Core 同時向總線進(jìn)行 RFO 廣播都想 Invalid 對方的 cache,總線會進(jìn)行仲裁,最終結(jié)果會是只有一個 Core 廣播成功,而另一個 Core 會失敗,其 cache 會被設(shè)置為 I 狀態(tài)。所以我們能看到,引入 cache 層后,原子操作由鎖內(nèi)存總線變?yōu)榱擞煽偩€仲裁來實(shí)現(xiàn)。

    如果聲明了 LOCK 指令前綴,那么對應(yīng)的 cache 地址會被總線鎖定,在上面的例子中,其他 Core 在訪問時會等到指令執(zhí)行結(jié)束后再進(jìn)行訪問,也即變?yōu)榱舜胁僮鳎瑢?shí)現(xiàn)了對 cache 讀寫的原子性。

    那么總結(jié)一下緩存鎖:在代碼指令前面聲明了 LOCK 指令前綴,想要原子訪問內(nèi)存數(shù)據(jù),如果內(nèi)存數(shù)據(jù)可以被緩存在 CPU 的 cache 中,運(yùn)行時通常不會在總線上產(chǎn)生 LOCK#信號,而是通過緩存一致性協(xié)議、總線仲裁機(jī)制與 cache 鎖定來阻止兩個或以上的 CPU 核,對同一塊地址的并發(fā)訪問。

    那么是不是所有的總線鎖都可以被優(yōu)化為緩存鎖呢?答案是否定的,不能被優(yōu)化的情況就是 split lock。

    1.4 Split lock

    由于緩存一致性協(xié)議的粒度是一個 cache line,當(dāng)原子操作的數(shù)據(jù)跨 cache line 時,依賴緩存鎖機(jī)制無法保證數(shù)據(jù)一致性,會退化為總線鎖來保證一致性,這種情況就是 split lock,split 也可以理解為訪存的 cache 被 split 為兩個 line。

    比如有如下數(shù)據(jù)結(jié)構(gòu):

    struct Data {
       char padding[62]; // 62字節(jié)
        int32_t value;  // 4字節(jié)
    } __attribute__((packed)) // 按實(shí)際字節(jié)對齊
    

    被緩存到 cache line 大小為 64 字節(jié)的 cache 中時,value 成員會跨 cache line。

    此時如果想要通過LOCK ADD 指令操作 Data 結(jié)構(gòu)中的 value 成員,就無法通過緩存鎖解決,只能走老路,鎖總線來保證數(shù)據(jù)一致性。

    而鎖總線會引起嚴(yán)重的性能下降,訪存延遲增加百倍左右,如果是內(nèi)存密集型業(yè)務(wù),性能會下降 2 個數(shù)量級。所以在現(xiàn)代 X86 處理器中,要避免寫出會產(chǎn)生 split lock 的代碼,并有能力檢測出 Split lock 的產(chǎn)生。

    2. 避免產(chǎn)生 Split lock

    回顧一下 Split lock 的產(chǎn)生條件:

    1. 對數(shù)據(jù)執(zhí)行原子訪問
    2. 要訪問的數(shù)據(jù)在 cache 中跨 cache line 存儲

    因?yàn)樵硬僮魇潜容^基礎(chǔ)的操作,所以我們以數(shù)據(jù)跨 cache line 存儲為介入點(diǎn)進(jìn)行分析。

    如果數(shù)據(jù)只存儲在一個 cache line 中,那就可以解決問題。

    2.1 編譯器優(yōu)化

    我們前面的數(shù)據(jù)結(jié)構(gòu)中有用到 __attribute__((packed)) 這個 GCC 的特性,表示不進(jìn)行內(nèi)存對齊優(yōu)化。

    如果不引入 __attribute__((packed)),使用內(nèi)存對齊優(yōu)化時,編譯器會對內(nèi)存數(shù)據(jù)進(jìn)行填充,比如在 padding 后填入 2 字節(jié),使 value 的內(nèi)存地址可以被 4 字節(jié)整除,從而達(dá)到對齊。被緩存到 cache 中時 value 也就不會跨 cache line 了。

    既然編譯器可以優(yōu)化后可以通過內(nèi)存對齊避免跨 cache line 訪問,為什么還要引入 __attribute__((packed))呢?

    這是因?yàn)橥ㄟ^ __attribute__((packed)) 強(qiáng)制按數(shù)據(jù)結(jié)構(gòu)對齊,也有好處。比如基于數(shù)據(jù)結(jié)構(gòu)的網(wǎng)絡(luò)通信,不需要填充多余字節(jié)等。

    2.2 注意事項(xiàng)

    我們在編寫代碼過程中,有以下幾點(diǎn)需要注意:

    1. 有條件的情況下,盡量使用編譯器的內(nèi)存對齊優(yōu)化。
    2. 在不能使用編譯器優(yōu)化時,考慮好結(jié)構(gòu)體成員的大小與聲明先后順序。
    3. 在產(chǎn)生可能不對齊的內(nèi)存訪問時,盡量不要使用原子指令來進(jìn)行訪問。

    3. Split lock 的檢測與處理

    3.1 使用場景

    1. 硬實(shí)時系統(tǒng):當(dāng)硬實(shí)時應(yīng)用運(yùn)行在一些核上,另一個普通程序運(yùn)行在其他核上,普通程序可以產(chǎn)生 bus lock 來打破硬實(shí)時的要求。
    2. 云計(jì)算:多租戶運(yùn)行在一個物理機(jī)上,一個虛擬機(jī)內(nèi)產(chǎn)生 bus lock 可以干擾其他虛擬機(jī)的性能。

    下面主要針對云環(huán)境,自底向上進(jìn)行分析。

    3.2 硬件檢測支持

    當(dāng)嘗試 split lock 操作時會產(chǎn)生 Alignment Check (#AC) exception,當(dāng)獲取 bus lock 并執(zhí)行后會產(chǎn)生 Debug(#DB) trap。

    硬件這里區(qū)分下了 split lock 與 bus lock:

    • split lock 指操作數(shù)跨兩個 cache line 的原子操作
    • bus lock 有兩類情況可以產(chǎn)生,要么是 writeback 內(nèi)存的 split lock,要么是非 writeback 內(nèi)存的任何 lock 操作

    概念上,split lock 是 bus lock 的一種,split lock 傾向于跨 cache line 訪問,bus lock 傾向鎖總線的操作。

    3.2.1 相關(guān)寄存器(MSR)

    發(fā)生 split lock 和 bus lock 時是否產(chǎn)生對應(yīng)的 exception,可以由特定的寄存器控制,下面是相關(guān)的控制寄存器。

    1. MSR_MEMORY_CTRL/MSR_TEST_CTRL:33H 這個 MSR 的 bit 29,控制 split lock 引起的#AC exception。

    2. IA32_DEBUGCTL:這個 MSR 的 bit 2,控制 bus lock 引起的#DB exception。

    3.3 內(nèi)核處理支持

    以 v5.17 版本為例進(jìn)行分析。當(dāng)前版本內(nèi)核支持一個相關(guān)啟動參數(shù) split_lock_detect,配置項(xiàng)和對應(yīng)功能如下:

    實(shí)現(xiàn) split_lock_detect 主要分為 3 部分:配置、初始化、處理,下面我們逐項(xiàng)分析一下源碼:

    3.3.1 配置

    -> start_kernel
        -> sld_setup
            -> split_lock_setup
                -> __split_lock_setup
            -> sld_state_setup
    

    內(nèi)核啟動時,會先進(jìn)行 sld(split lock detect)的 setup。

    static void __init __split_lock_setup(void)
    {
            if (!split_lock_verify_msr(false)) {
                    pr_info("MSR access failed: Disabled\n");
                    return;
            }
    
            rdmsrl(MSR_TEST_CTRL, msr_test_ctrl_cache);
    
            if (!split_lock_verify_msr(true)) {
                    pr_info("MSR access failed: Disabled\n");
                    return;
            }
    
            /* Restore the MSR to its cached value. */
            wrmsrl(MSR_TEST_CTRL, msr_test_ctrl_cache);
    
            setup_force_cpu_cap(X86_FEATURE_SPLIT_LOCK_DETECT);
    }
    
    static bool split_lock_verify_msr(bool on)
    {
            u64 ctrl, tmp;
    
            if (rdmsrl_safe(MSR_TEST_CTRL, &ctrl))
                    return false;
            if (on)
                    ctrl |=MSR_TEST_CTRL_SPLIT_LOCK_DETECT;
            else
                    ctrl &=~MSR_TEST_CTRL_SPLIT_LOCK_DETECT;
            if (wrmsrl_safe(MSR_TEST_CTRL, ctrl))
                    return false;
            rdmsrl(MSR_TEST_CTRL, tmp);
            return ctrl==tmp;
    }
    
    #define MSR_TEST_CTRL               0x00000033
    

    __split_lock_setup 中嘗試 enable/disable 33H MSR 進(jìn)行 verify,結(jié)束也并沒有 enable split lock #AC exception,而是僅留下一個全局變量 msr_test_ctrl_cache 當(dāng)作后面操作這個 MSR 的 cache 使用。

    static void __init sld_state_setup(void)
    {
            enum split_lock_detect_state state=sld_warn; // 默認(rèn)配置
            char arg[20];
            int i, ret;
    
            if (!boot_cpu_has(X86_FEATURE_SPLIT_LOCK_DETECT) &&
                !boot_cpu_has(X86_FEATURE_BUS_LOCK_DETECT))
                    return;
    
            ret=cmdline_find_option(boot_command_line, "split_lock_detect",
                                      arg, sizeof(arg));
            if (ret >=0) {
                    for (i=0; i < ARRAY_SIZE(sld_options); i++) {
                            if (match_option(arg, ret, sld_options[i].option)) {
                                    state=sld_options[i].state;
                                    break;
                            }
                    }
            }
            sld_state=state;
    }
    
    static inline bool match_option(const char *arg, int arglen, const char *opt)
    {
            int len=strlen(opt), ratelimit;
    
            if (strncmp(arg, opt, len))
                    return false;
    
            /*
             * Min ratelimit is 1 bus lock/sec.
             * Max ratelimit is 1000 bus locks/sec.
             */
            if (sscanf(arg, "ratelimit:%d", &ratelimit)==1 &&
                ratelimit > 0 && ratelimit <=1000) {
                    ratelimit_state_init(&bld_ratelimit, HZ, ratelimit);
                    ratelimit_set_flags(&bld_ratelimit, RATELIMIT_MSG_ON_RELEASE);
                    return true;
            }
    
            return len==arglen;
    }
    

    sld_state_setup 做的是解析出內(nèi)核啟動參數(shù) split_lock_detect 的配置(可以看到默認(rèn)的配置是 warn 級別),如果是 ratelimit 配置,會使用內(nèi)核的 ratelimit 庫初始化一個 bld_ratelimit 全局變量給 handle 階段用。

    3.3.2 初始化

    -> start_kernel
        -> init_intel
            -> split_lock_init
            -> bus_lock_init
    

    setup 完成基本的 verify 和獲取啟動參數(shù)配置后,會嘗試進(jìn)行硬件 enbale 操作。

    3.3.2.1 split lock init

    static void split_lock_init(void)
    {
            /*
             * #DB for bus lock handles ratelimit and #AC for split lock is
             * disabled.
             */
            if (sld_state==sld_ratelimit) {
                    split_lock_verify_msr(false);
                    return;
            }
    
            if (cpu_model_supports_sld)
                    split_lock_verify_msr(sld_state !=sld_off);
    }
    

    split lock 的 init 中,如果發(fā)現(xiàn)配置的參數(shù)是 ratelimit,會 disable split lock 的硬件檢測。其他非 off 參數(shù)(warn、fatal)則會 enable 硬件。

    3.3.2.2 bus lock init

    static void bus_lock_init(void)
    {
            u64 val;
    
            /*
             * Warn and fatal are handled by #AC for split lock if #AC for
             * split lock is supported.
             */
            if (!boot_cpu_has(X86_FEATURE_BUS_LOCK_DETECT) ||
                (boot_cpu_has(X86_FEATURE_SPLIT_LOCK_DETECT) &&
                (sld_state==sld_warn || sld_state==sld_fatal)) ||
                sld_state==sld_off)
                    return;
    
            /*
             * Enable #DB for bus lock. All bus locks are handled in #DB except
             * split locks are handled in #AC in the fatal case.
             */
            rdmsrl(MSR_IA32_DEBUGCTLMSR, val);
            val |=DEBUGCTLMSR_BUS_LOCK_DETECT;
            wrmsrl(MSR_IA32_DEBUGCTLMSR, val);
    }
    
    #define DEBUGCTLMSR_BUS_LOCK_DETECT (1UL <<  2)
    

    bus lock 的 init 中,如果 CPU 不支持 bus lock 則不會 enable bus lock 的硬件檢測,如果 CPU 同時支持 split lock 與 bus lock 的硬件檢測并且配置參數(shù)還是 warn 或 fatal 時也不會 enable 硬件。

    所以從代碼看 enable CPU 的 bus lock 檢測情況有兩種:一是 CPU 支持 bus lock 檢測的情況下并且配置參數(shù)指定為 ratelimt,二是 CPU 不支持 split lock 檢測但支持 bus lock 檢測并且配置參數(shù)非 off。

    3.3.3 處理

    3.3.3.1 split lock handle

    DEFINE_IDTENTRY_ERRORCODE(exc_alignment_check)
    {
            char *str="alignment check";
    
            if (notify_die(DIE_TRAP, str, regs, error_code, X86_TRAP_AC, SIGBUS)==NOTIFY_STOP)
                    return;
    
            if (!user_mode(regs)) // 如果不是用戶態(tài)
                    die("Split lock detected\n", regs, error_code);
    
            local_irq_enable();
    
            if (handle_user_split_lock(regs, error_code))
                    goto out;
    
            do_trap(X86_TRAP_AC, SIGBUS, "alignment check", regs,
                    error_code, BUS_ADRALN, NULL);
    
    out:
            local_irq_disable();
    }
    
    
    bool handle_user_split_lock(struct pt_regs *regs, long error_code)
    {
            if ((regs->flags & X86_EFLAGS_AC) || sld_state==sld_fatal)
                    return false;
            split_lock_warn(regs->ip);
            return true;
    }
    
    static void split_lock_warn(unsigned long ip)
    {
            pr_warn_ratelimited("#AC: %s/%d took a split_lock trap at address: 0x%lx\n",
                                current->comm, current->pid, ip);
    
            /*
             * Disable the split lock detection for this task so it can make
             * progress and set TIF_SLD so the detection is re-enabled via
             * switch_to_sld() when the task is scheduled out.
             */
            sld_update_msr(false);
            set_tsk_thread_flag(current, TIF_SLD);
    }
    

    #AC exception 產(chǎn)生后,如果不是用戶態(tài),會調(diào)用 die,進(jìn)入 panic 流程。

    如果是在用戶態(tài),那么配置為 fatal 時就會向當(dāng)前用戶態(tài)進(jìn)程發(fā)送 SIGBUS 信號,用戶態(tài)進(jìn)程不手動捕獲 SIGBUS 就會被 kill。

    如果是用戶態(tài),配置為 warn 時,會打印一條警告日志并輸出當(dāng)前進(jìn)程信息,同時 disable split lock 的檢測,并通過設(shè)置當(dāng)前進(jìn)程 flags 的 TIF_SLD 位表示這個進(jìn)程已經(jīng)被檢測過一次了。

    -> context_switch
        -> __switch_to_xtra
    
    void __switch_to_xtra(struct task_struct *prev_p, struct task_struct *next_p)
    {
            unsigned long tifp, tifn;
    
            tifn=read_task_thread_flags(next_p);
            tifp=read_task_thread_flags(prev_p);
    
            if ((tifp ^ tifn) & _TIF_SLD)
                    switch_to_sld(tifn);
    }
    
    void switch_to_sld(unsigned long tifn)
    {
            sld_update_msr(!(tifn & _TIF_SLD));
    }
    

    在 context_switch 進(jìn)行進(jìn)程切換時,如果 prev 進(jìn)程和 next 進(jìn)程的 flags 中 TIF_SLD 位不同,那么就會進(jìn)行 split lock detect 的切換,切換的依據(jù)是 next 進(jìn)程的 TIF_SLD 位。

    舉個例子,CPU 0 上進(jìn)程 A 在觸發(fā)了一次 split lock 的 warn 檢測后,CPU 0 的 split lock 檢測會被 disable,避免頻繁產(chǎn)生警告日志,進(jìn)程 A 的 flags 置位 TIF_SLD,進(jìn)程 A 執(zhí)行完成后,切換到進(jìn)程 B 運(yùn)行在 CPU 0 上,切換時由于 A 與 B 的 flags 的 TIF_SLD 位不同,我們就再根據(jù)進(jìn)程 B 的 flags 來 enable split lock 的檢測,進(jìn)程 B 執(zhí)行完成后,再次切換到進(jìn)程 A 運(yùn)行時,會再 disable split lock。

    上面的機(jī)制就實(shí)現(xiàn)了每個進(jìn)程只 warn once 的需求。

    3.3.3.2 bus lock handle

    static __always_inline void exc_debug_user(struct pt_regs *regs,
                                               unsigned long dr6)
    {
            /* #DB for bus lock can only be triggered from userspace. */
            if (dr6 & DR_BUS_LOCK)
                    handle_bus_lock(regs);
    }
    
    void handle_bus_lock(struct pt_regs *regs)
    {
            switch (sld_state) {
            case sld_off:
                    break;
            case sld_ratelimit:
                    /* Enforce no more than bld_ratelimit bus locks/sec. */
                    while (!__ratelimit(&bld_ratelimit))
                            msleep(20);
                    /* Warn on the bus lock. */
                    fallthrough;
            case sld_warn:
                    pr_warn_ratelimited("#DB: %s/%d took a bus_lock trap at address: 0x%lx\n",
                                        current->comm, current->pid, regs->ip);
                    break;
            case sld_fatal:
                    force_sig_fault(SIGBUS, BUS_ADRALN, NULL);
                    break;
            }
    }
    

    #DB trap 產(chǎn)生后,warn 和 fatal 的流程和#AC 基本差不多,我們這里重點(diǎn)分析 ratelimit,這也是引入 bus lock 的一個強(qiáng)需求。

    強(qiáng)制把 bus lock 產(chǎn)生的頻率降低到配置的 ratelimit。原理就是如果頻率超出設(shè)定,就直接 sleep 20 ms,直到頻率降下來(這里的頻率是整個系統(tǒng)產(chǎn)生 bus lock 的頻率)。

    降頻過后通過編譯器的 fallthrough 流入 sld_warn case 產(chǎn)生一條警告日志。

    4. 虛擬化環(huán)境的檢測與處理

    前面的分析都是針對物理機(jī)上的內(nèi)核與用戶態(tài)程序(VMX root mode)。在虛擬化環(huán)境中需要再考慮一些問題,比如如果 split lock 來自于 Guest 中,Host 如何檢測?怎么避免對其他 Guest 的影響?如果直接在 Host 上 enable bus lock ratelimit,可能會影響沒有準(zhǔn)備好的 Guest。如果直接把 split lock 的檢測開關(guān)暴露給 Guest,Host 或其他 Guest 怎么處理等等。

    CPU 可以在 VMX mode 下支持 split lock 檢測的#AC trap,后面具體怎么做由 hypervisor 決定。大部分 hypervisor 會直接把 trap 轉(zhuǎn)發(fā)到 Guest 中,如果 Guest 沒有準(zhǔn)備好,可能會產(chǎn)生 crash。之前的 VMX 版本中就有這個問題。

    因此首先就要讓 hypervisor 正確處理 trap。

    4.1 虛擬化環(huán)境處理流程

    下面是虛擬化環(huán)境對 split lock 與 bus lock 的整體處理流程圖:

    Guest 中嘗試 split lock 操作,會 vm exit 到 kvm 中,如果硬件不支持 split lock 檢測或者為 legacy #AC,會把#AC 注入給 Guest 處理,如果硬件支持檢測,那么會根據(jù)配置產(chǎn)生警告,甚至嘗試 SIGBUS。

    Guest 進(jìn)行 bus lock 后,會 vm exit 到 kvm 中,kvm 通知 QEMU,vCPU 線程會主動進(jìn)行 sleep 降頻。

    下面我們再自底向上進(jìn)行分析。

    4.2 硬件支持

    4.2.1 split lock

    #AC exception 被包含在 NMI exit reason 里面。

    4.2.2 bus lock

    在 VMX non-root mode 下,CPU 檢測到 bus lock 后會產(chǎn)生 VM exit,reason 為 74。

    4.3 KVM 支持

    -> vmx_handle_exit
        -> __vmx_handle_exit
            -> kvm_vmx_exit_handlers
                -> handle_exception_nmi // split lock
                -> handle_bus_lock_vmexit // bus lock
    

    4.3.1 split lock

    static int handle_exception_nmi(struct kvm_vcpu *vcpu)
    {
            switch (ex_no) {
            case AC_VECTOR:
                    if (vmx_guest_inject_ac(vcpu)) {
                            kvm_queue_exception_e(vcpu, AC_VECTOR, error_code);
                            return 1;
                    }
    
                    /*
                     * Handle split lock. Depending on detection mode this will
                     * either warn and disable split lock detection for this
                     * task or force SIGBUS on it.
                     */
                    if (handle_guest_split_lock(kvm_rip_read(vcpu)))
                            return 1;
    }
    
    // Guest handle
    bool vmx_guest_inject_ac(struct kvm_vcpu *vcpu)
    {
            if (!boot_cpu_has(X86_FEATURE_SPLIT_LOCK_DETECT))
                    return true;
    
            return vmx_get_cpl(vcpu)==3 && kvm_read_cr0_bits(vcpu, X86_CR0_AM) &&
                   (kvm_get_rflags(vcpu) & X86_EFLAGS_AC);
    }
    
    // KVM handle
    bool handle_guest_split_lock(unsigned long ip)
    {
            if (sld_state==sld_warn) {
                    split_lock_warn(ip);
                    return true;
            }
    
            pr_warn_once("#AC: %s/%d %s split_lock trap at address: 0x%lx\n",
                         current->comm, current->pid,
                         sld_state==sld_fatal ? "fatal" : "bogus", ip);
    
            current->thread.error_code=0;
            current->thread.trap_nr=X86_TRAP_AC;
            force_sig_fault(SIGBUS, BUS_ADRALN, NULL);
            return false;
    }
    

    Guest 內(nèi)部產(chǎn)生 split lock 操作時,由于是#AC exception,會 VM exit 出來。

    這里首先要說明一下#AC exception 本身有兩種類型:

    1. legacy #AC exception
    2. split lock #AC exception

    KVM 根據(jù) Host 和 Guest 狀態(tài)最終產(chǎn)生兩種行為:

    1. 讓 Guest 處理:把#AC 注入到 Guest 中。
    2. 如果硬件不支持 split lock 檢測,無條件注入到 Guest 中。
    3. 如果 Host enable 了 split lock 檢測,則只有在產(chǎn)生#AC exception 為 Guest 用戶態(tài)且為 legacy #AC 情況下才會注入到 Guest 中。
    4. 讓 HOST 處理:產(chǎn)生警告甚至發(fā)送 SIGBUS。
    5. 不注入到 Guest 中時,Host 如果配置的是 warn 則每個 vCPU 線程只 warn 一次,如果配置的是 fatal 則產(chǎn)生 SIGBUS。

    4.3.2 bus lock

    #define EXIT_REASON_BUS_LOCK            74
    
    static int (*kvm_vmx_exit_handlers[])(struct kvm_vcpu *vcpu)={
        [EXIT_REASON_BUS_LOCK]      =handle_bus_lock_vmexit,
    }
    
    static int handle_bus_lock_vmexit(struct kvm_vcpu *vcpu)
    {
            /*
             * Hardware may or may not set the BUS_LOCK_DETECTED flag on BUS_LOCK
             * VM-Exits. Unconditionally set the flag here and leave the handling to
             * vmx_handle_exit().
             */
            to_vmx(vcpu)->exit_reason.bus_lock_detected=true;
            return 1;
    }
    
    union vmx_exit_reason {
            struct {
                    u32        basic                        : 16;
                    u32        reserved16                : 1;
                    u32        reserved17                : 1;
                    u32        reserved18                : 1;
                    u32        reserved19                : 1;
                    u32        reserved20                : 1;
                    u32        reserved21                : 1;
                    u32        reserved22                : 1;
                    u32        reserved23                : 1;
                    u32        reserved24                : 1;
                    u32        reserved25                : 1;
                    u32        bus_lock_detected        : 1;
                    u32        enclave_mode                : 1;
                    u32        smi_pending_mtf                : 1;
                    u32        smi_from_vmx_root        : 1;
                    u32        reserved30                : 1;
                    u32        failed_vmentry                : 1;
            };
            u32 full;
    };
    

    VM exit 后,執(zhí)行根據(jù) reason 74 索引的函數(shù) handle_bus_lock_vmexit,其中僅僅是把 exit_reason 的 bus_lock_detected 置位。

    static int vmx_handle_exit(struct kvm_vcpu *vcpu, fastpath_t exit_fastpath)
    {
            int ret=__vmx_handle_exit(vcpu, exit_fastpath);
    
            /*
             * Exit to user space when bus lock detected to inform that there is
             * a bus lock in guest.
             */
            if (to_vmx(vcpu)->exit_reason.bus_lock_detected) {
                    if (ret > 0)
                            vcpu->run->exit_reason=KVM_EXIT_X86_BUS_LOCK;
    
                    vcpu->run->flags |=KVM_RUN_X86_BUS_LOCK;
                    return 0;
            }
            return ret;
    }
    

    執(zhí)行完__vmx_handle_exit 后,檢測到 bus_lock_detected 置位后會設(shè)置返回給用戶態(tài)的 exit_reason 和 flags。

    4.4 QEMU 支持

    以 v6.2.0 版本為例進(jìn)行分析:

    -> kvm_cpu_exec
        -> kvm_vcpu_ioctl
        -> kvm_arch_post_run
        -> kvm_arch_handle_exit
    
    int kvm_arch_handle_exit(CPUState *cs, struct kvm_run *run)
    {
        switch (run->exit_reason) {
            case KVM_EXIT_X86_BUS_LOCK:
                /* already handled in kvm_arch_post_run */
                ret=0;
                break;
        }
    }
    
    MemTxAttrs kvm_arch_post_run(CPUState *cpu, struct kvm_run *run) {
        if (run->flags & KVM_RUN_X86_BUS_LOCK) {
            kvm_rate_limit_on_bus_lock();
        }
    }
    
    static void kvm_rate_limit_on_bus_lock(void)
    {
        uint64_t delay_ns=ratelimit_calculate_delay(&bus_lock_ratelimit_ctrl, 1);
    
        if (delay_ns) {
            g_usleep(delay_ns / SCALE_US);
        }
    }
    

    QEMU 通過 KVM 返回值了解到 Guest 中有 bus lock 產(chǎn)生,進(jìn)入 kvm_rate_limit_on_bus_lock 中,也是通過 sleep 實(shí)現(xiàn) ratelimit,達(dá)到降低 Guest 產(chǎn)生 bus lock 頻率的效果。

    Host 上的 ratelimit 是通過 split_lock_detect 啟動參數(shù)來控制的,那么 Guest 呢?

    int kvm_arch_init(MachineState *ms, KVMState *s) {
        if (object_dynamic_cast(OBJECT(ms), TYPE_X86_MACHINE)) {
            X86MachineState *x86ms=X86_MACHINE(ms);
    
            if (x86ms->bus_lock_ratelimit > 0) {
                ret=kvm_check_extension(s, KVM_CAP_X86_BUS_LOCK_EXIT);
                if (!(ret & KVM_BUS_LOCK_DETECTION_EXIT)) {
                    error_report("kvm: bus lock detection unsupported");
                    return -ENOTSUP;
                }
                ret=kvm_vm_enable_cap(s, KVM_CAP_X86_BUS_LOCK_EXIT, 0,
                                        KVM_BUS_LOCK_DETECTION_EXIT);
                if (ret < 0) {
                    error_report("kvm: Failed to enable bus lock detection cap: %s",
                                 strerror(-ret));
                    return ret;
                }
                ratelimit_init(&bus_lock_ratelimit_ctrl);
                ratelimit_set_speed(&bus_lock_ratelimit_ctrl,
                                    x86ms->bus_lock_ratelimit, BUS_LOCK_SLICE_TIME);
            }
        }
    }
    
    static void x86_machine_get_bus_lock_ratelimit(Object *obj, Visitor *v,
                                    const char *name, void *opaque, Error **errp)
    {
        X86MachineState *x86ms=X86_MACHINE(obj);
        uint64_t bus_lock_ratelimit=x86ms->bus_lock_ratelimit;
    
        visit_type_uint64(v, name, &bus_lock_ratelimit, errp);
    }
    
    static void x86_machine_class_init(ObjectClass *oc, void *data)
    {
        object_class_property_add(oc, X86_MACHINE_BUS_LOCK_RATELIMIT, "uint64_t",
                                    x86_machine_get_bus_lock_ratelimit,
                                    x86_machine_set_bus_lock_ratelimit, NULL, NULL);
    }
    
    #define X86_MACHINE_BUS_LOCK_RATELIMIT  "bus-lock-ratelimit"
    

    QEMU 對 Guest bus lock 的 ratelimit 從啟動參數(shù) bus-lock-ratelimit 中獲取。

    4.5 Libvirt 支持

    Libvirt 支持 QEMU 的 bus-lock-ratelimit 啟動參數(shù)目前還沒 upsteam:

    https://listman.redhat.com/archives/libvir-list/2021-December/225755.html

    5. 總結(jié)

    由于 X86 硬件的特性,支持跨 cache line 的原子語義,實(shí)現(xiàn)上需要用 split lock 維持原子性,但這卻需要以整個系統(tǒng)的訪存性能為代價。開發(fā)者逐漸意識到這個 feature 害人不淺,盡量不去使用,比如內(nèi)核的開發(fā)者保證自己不會產(chǎn)生 split lock,甚至不惜內(nèi)核 panic。而用戶態(tài)程序則會產(chǎn)生警告,然后降低程序的執(zhí)行頻率或者由內(nèi)核進(jìn)行 kill。虛擬化環(huán)境由于軟件棧較多,會盡量由 Host 側(cè) KVM 與 QEMU 處理,進(jìn)行告警甚至 kill 虛擬機(jī),或通知 QEMU 進(jìn)行降頻。

    References

    1. https://www.agner.org/optimize/instruction_tables.pdf
    2. https://www.kernel.org/doc/html/latest/x86/buslock.html
    3. https://en.wikipedia.org/wiki/Cache_coherence
    4. https://lwn.net/Articles/790464/
    5. https://lwn.net/Articles/850011/
    6. https://lwn.net/Articles/816918/
    7. https://lore.kernel.org/all/20200410115517.176308876@linutronix.de/T/#mb0a765c7b9799d1a06d54d31f4a47db15c01ecde
    8. https://lore.kernel.org/all/20200131200134.GD18946@linux.intel.com/T/#u
網(wǎng)站首頁   |    關(guān)于我們   |    公司新聞   |    產(chǎn)品方案   |    用戶案例   |    售后服務(wù)   |    合作伙伴   |    人才招聘   |   

友情鏈接: 餐飲加盟

地址:北京市海淀區(qū)    電話:010-     郵箱:@126.com

備案號:冀ICP備2024067069號-3 北京科技有限公司版權(quán)所有