Docker學習筆記之Copy on Write機制


 

0x00 概述

Copy-On-Write簡稱COW,是一種用於程序設計中的優化策略。其基本思路是,從一開始大家都在共享同一個內容,當某個人想要修改這個內容的時候,才會真正把內容Copy出去形成一個新的內容然后再改,這是一種延時懶惰策略。從JDK1.5開始Java並發包里提供了兩個使用CopyOnWrite機制實現的並發容器,它們是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器非常有用,可以在非常多的並發場景中使用到。

 

0x01 什么是CopyOnWrite容器

CopyOnWrite容器即寫時復制的容器。通俗的理解是當我們往一個容器添加元素的時候,不直接往當前容器添加,而是先將當前容器進行Copy,復制出一個新的容器,然后新的容器里添加元素,添加完元素之后,再將原容器的引用指向新的容器。這樣做的好處是我們可以對CopyOnWrite容器進行並發的讀,而不需要加鎖,因為當前容器不會添加任何元素。所以CopyOnWrite容器也是一種讀寫分離的思想,讀和寫不同的容器。

 

linux內核在使用fork創建進程時,基本上會使用Copy-On-Write(COW)技術。這里解釋一下COW技術以及為什么在fork中使用。

WIKI上對COW的解釋:

Copy-on-write (sometimes referred to as "COW") is an optimization strategy used in computer programming. The fundamental idea is that if multiple callers ask for resources which are initially indistinguishable, they can all be given pointers to the same resource. This function can be maintained until a caller tries to modify its "copy" of the resource, at which point a true private copy is created to prevent the changes becoming visible to everyone else. All of this happens transparently to the callers. The primary advantage is that if a caller never makes any modifications, no private copy need ever be created.

意思上就是:在復制一個對象的時候並不是真正的把原先的對象復制到內存的另外一個位置上,而是在新對象的內存映射表中設置一個指針,指向源對象的位置,並把那塊內存的Copy-On-Write位設置為1.

這樣,在對新的對象執行讀操作的時候,內存數據不發生任何變動,直接執行讀操作;而在對新的對象執行寫操作時,將真正的對象復制到新的內存地址中,並修改新對象的內存映射表指向這個新的位置,並在新的內存位置上執行寫操作。

這個技術需要跟虛擬內存和分頁同時使用,好處就是在執行復制操作時因為不是真正的內存復制,而只是建立了一個指針,因而大大提高效率。但這不是一直成立的,如果在復制新對象之后,大部分對象都還需要繼續進行寫操作會產生大量的分頁錯誤,得不償失。所以COW高效的情況只是在復制新對象之后,在一小部分的內存分頁上進行寫操作。

 

COW詳解可以參考這篇博文

 

COW在編程中被廣泛應用。

特別是在操作系統當中,當一個程序運行結束時,操作系統並不會急着把其清除出內存,原因是有可能程序還會馬上再運行一次(從磁盤把程序裝入到內存是個很慢的過程),而只有當內存不夠用了,才會把這些還駐留內存的程序清出。

而對於Linux內核空間創建進程時的fork,由於在內核空間已經由代碼決定不使用COW技術(參見mm/memory.c Line 221)。從而由內核空間的進程0(main)創建進程1(init)不使用COW,系統對此次新進程創建進行了特殊處理(存在疑問,同樣是fork,如何實現對這個fork的特殊處理,估計是schedule,看到再解決了)。進程0和進程1同時使用着內核代碼區內(<=1M)相同的代碼和數據內存頁面(640KB),只是執行代碼不在一處,因此他們也同時使用着相同的用戶堆棧區。在為進程1(init)復制其父進程(進程0)的頁目錄和頁表項時,進程0的640KB頁表項的屬性沒有改動過(仍然可讀寫),但是進程1的640KB對應的頁表項卻被設置成只讀。因此當進程1(init)開始執行時,對用戶堆棧的入棧操作將導致頁面寫保護異常,從而使得內核的內存管理程序為進程1在主內存區中分配一內存頁面,並把進程0中的頁面內容復制到新的頁面上。從此時開始,進程1開始有自己獨立的內存頁面,由於此時的內存頁面在主內存區,因此進程1中繼續創建新的子進程時可以采用COW技術。

在Linux內核首先通過move_to_user_mode轉移到用戶模式下執行,至此main函數就以進程0的身份運行。而進程0是所有將創建進程的父進程,他創建進程1(init)時,fork的結果就是進程1與進程0擁有完全相同的內存空間、堆棧,這時進程0和進程1的內存還都在Linux內核空間中。

內核調度進程運行時次序是隨機的,有可能在進程0創建了進城1之后仍然允許進程0,由於兩個進程共享內存空間,為了不出現沖突問題,就必須要求進程0在進程1執行堆棧操作(進程1的堆棧操作會導致頁面保護異常,從而使得進程1在主內存區得到新的用戶頁面區,此時進程1和進程0才算是真正獨立,如前面所述)之前禁止使用用戶堆棧區。所以進程0在執行了fork(創建了進程1)之后的pause使用內嵌的方式,保證進程0(main)不會弄亂堆棧。

進程1中如果執行fork以及exec,此時的頁面空間已經到了主內存區,就可以使用COW了。

 

0x02 CopyOnWriteArrayList的實現原理

 在使用CopyOnWriteArrayList之前,我們先閱讀其源碼了解下它是如何實現的。以下代碼是向ArrayList里添加元素,可以發現在添加的時候是需要加鎖的,否則多線程寫的時候會Copy出N個副本出來。

public boolean add(T e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
 
        Object[] elements = getArray();
 
        int len = elements.length;
        // 復制出新數組
 
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 把新元素添加到新數組里
 
        newElements[len] = e;
        // 把原數組引用指向新數組
 
        setArray(newElements);
 
        return true;
 
    } finally {
 
        lock.unlock();
 
    }
 
}
 
final void setArray(Object[] a) {
    array = a;
}

讀的時候不需要加鎖,如果讀的時候有多個線程正在向ArrayList添加數據,讀還是會讀到舊的數據,因為寫的時候不會鎖住舊的ArrayList。

public E get(int index) {
    return get(getArray(), index);
}

JDK中並沒有提供CopyOnWriteMap,我們可以參考CopyOnWriteArrayList來實現一個,基本代碼如下:

import java.util.Collection;
import java.util.Map;
import java.util.Set;
 
public class CopyOnWriteMap<K, V> implements Map<K, V>, Cloneable {
    private volatile Map<K, V> internalMap;
 
    public CopyOnWriteMap() {
        internalMap = new HashMap<K, V>();
    }
 
    public V put(K key, V value) {
 
        synchronized (this) {
            Map<K, V> newMap = new HashMap<K, V>(internalMap);
            V val = newMap.put(key, value);
            internalMap = newMap;
            return val;
        }
    }
 
    public V get(Object key) {
        return internalMap.get(key);
    }
 
    public void putAll(Map<? extends K, ? extends V> newData) {
        synchronized (this) {
            Map<K, V> newMap = new HashMap<K, V>(internalMap);
            newMap.putAll(newData);
            internalMap = newMap;
        }
    }
}

實現很簡單,只要了解了CopyOnWrite機制,我們可以實現各種CopyOnWrite容器,並且在不同的應用場景中使用。

 

0x03 CopyOnWrite的應用場景

CopyOnWrite並發容器用於讀多寫少的並發場景。比如白名單,黑名單,商品類目的訪問和更新場景,假如我們有一個搜索網站,用戶在這個網站的搜索框中,輸入關鍵字搜索內容,但是某些關鍵字不允許被搜索。這些不能被搜索的關鍵字會被放在一個黑名單當中,黑名單每天晚上更新一次。當用戶搜索時,會檢查當前關鍵字在不在黑名單當中,如果在,則提示不能搜索。實現代碼如下:

package com.ifeve.book;
 
import java.util.Map;
 
import com.ifeve.book.forkjoin.CopyOnWriteMap;
 
/**
 * 黑名單服務
 *
 * @author fangtengfei
 *
 */
public class BlackListServiceImpl {
 
    private static CopyOnWriteMap<String, Boolean> blackListMap = new CopyOnWriteMap<String, Boolean>(
            1000);
 
    public static boolean isBlackList(String id) {
        return blackListMap.get(id) == null ? false : true;
    }
 
    public static void addBlackList(String id) {
        blackListMap.put(id, Boolean.TRUE);
    }
 
    /**
     * 批量添加黑名單
     *
     * @param ids
     */
    public static void addBlackList(Map<String,Boolean> ids) {
        blackListMap.putAll(ids);
    }
 
}

代碼很簡單,但是使用CopyOnWriteMap需要注意兩件事情:

1. 減少擴容開銷。根據實際需要,初始化CopyOnWriteMap的大小,避免寫時CopyOnWriteMap擴容的開銷。

2. 使用批量添加。因為每次添加,容器每次都會進行復制,所以減少添加次數,可以減少容器的復制次數。如使用上面代碼里的addBlackList方法。

 

0x04 CopyOnWrite的缺點

CopyOnWrite容器有很多優點,但是同時也存在兩個問題,即內存占用問題和數據一致性問題。所以在開發的時候需要注意一下。

內存占用問題。因為CopyOnWrite的寫時復制機制,所以在進行寫操作的時候,內存里會同時駐扎兩個對象的內存,舊的對象和新寫入的對象(注意:在復制的時候只是復制容器里的引用,只是在寫的時候會創建新對象添加到新容器里,而舊容器的對象還在使用,所以有兩份對象內存)。如果這些對象占用的內存比較大,比如說200M左右,那么再寫入100M數據進去,內存就會占用300M,那么這個時候很有可能造成頻繁的Yong GC和Full GC。之前我們系統中使用了一個服務由於每晚使用CopyOnWrite機制更新大對象,造成了每晚15秒的Full GC,應用響應時間也隨之變長。

針對內存占用問題,可以通過壓縮容器中的元素的方法來減少大對象的內存消耗,比如,如果元素全是10進制的數字,可以考慮把它壓縮成36進制或64進制。或者不使用CopyOnWrite容器,而使用其他的並發容器,如ConcurrentHashMap

數據一致性問題。CopyOnWrite容器只能保證數據的最終一致性,不能保證數據的實時一致性。所以如果你希望寫入的的數據,馬上能讀到,請不要使用CopyOnWrite容器。

關於C++的STL中,曾經也有過Copy-On-Write的玩法,參見陳皓的《C++ STL String類中的Copy-On-Write》,后來,因為有很多線程安全上的事,就被去掉了。

 

該篇文章摘自網絡。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM