Продажа квадроциклов, снегоходов и мототехники
second logo
Пн-Чт: 10:00-20:00
Пт-Сб: 10:00-19:00 Вс: выходной

+7 (812) 924 3 942

+7 (911) 924 3 942

Copy-on-write / Хабр

Познакомился я с этой темой на одном из собеседований, когда мне задали вопрос о том, что есть какая-то большая структура и сколько памяти будет занято, если переменную с экземпляром этой структуры присвоить другой переменной. Тогда, вспоминая о том, что происходит при взятие подстроки, я ответил, что объем памяти не изменится и ошибся. Ну и уже после собеседования полез читать о copy-on-write (далее COW).

В этой статье мы обсудим эту тему и она станет очередным пунктом в том, чем отличаются ссылочные типы от типов значений в языке Swift. Как и говорилось выше, одним из таких отличий является наличие функционала COW, о котором и будем говорить.

Как известно основным отличием ссылочных типов от типов значений является то, что первые передаются по ссылкам, в то время как вторые копируются. У этого есть ряд своих плюсов таких как то, что типы значений работают быстрее, потому что хранятся на стеке, а не в куче, используют статическую диспетчеризацию и прочее.

Однако можно задаться вопросом “а зачем копировать данные, если мы их не меняем?”. Действительно и COW как раз-таки отвечает на этот вопрос и говорит, что это не обязательно.

Проведем небольшой эксперимент:

func address(o: UnsafeRawPointer) { 
  print("address: \(Int(bitPattern: o))")
}
var arr1 = [1, 2, 3]
var arr2 = arr1
address(arr1)
address(arr2)

Как думаете, что выведет?
А выведет то, чего мы и хотели, задавая вопрос:

address: 105553133388704
address: 105553133388704

Обе переменные ссылаются на одну область памяти, хотя мы и работаем с типом значений и данные первой переменной должны были бы копироваться во вторую.
А теперь изменим второй массив и посмотрим что будет.

func address(o: UnsafeRawPointer) { 
  print("address: \(Int(bitPattern: o))")
}
var arr1 = [1, 2, 3]
var arr2 = arr1
arr2.append(4)
address(arr1) 
address(arr2)

address: 105553154755744
address: 105553135788672

Как можно заметить теперь ссылки ведут на разные области в памяти.

Что нам это дает?

Благодаря этим экспериментам становится ясно, что swift предоставляет механизм, который копирует поведение ссылочных типов на типы значений до первых изменений, такой механизм и называется COW. Это избавляет нашу программу от лишних копирований, тем самым улучшая производительность.

Увы, но даже не для всех структур из стандартной библиотеки он реализован, но точно реализован для основных типов, которые при копирование могут затратить большое количество ресурсов, а именно: String, Array, Dictionary и Set.

Становится очевидно, что для наших с вами собственных структур такой механизм тем более не предусмотрен, но — не беда, мы можем сделать его сами.

Собственный COW

final class Ref<T> { 
  var val: T
	
  init(v: T) { 
    val = v
  } 
}
struct Box<T> {
  var ref: Ref<T>
	
  init(x: T) { 
    ref = Ref(x)
  }
	var value: T { 
    get {
      ref.val 
    }
    set {
      if (!isKnownUniquelyReferenced(&ref)) {
        ref = Ref(newValue) 
      } else {
        ref. val = newValue
      }
    } 
  }
}

Одним из важных моментов этого кода является метод:

func isKnownUniquelyReferenced<T> (_ object: inout T?) -> Bool where T : AnyObject

Returns a Boolean value indicating whether the given object is known to have a single strong reference.

Данная реализация хранит наше value (в Box) одинаковым при переиспользование. Посмотрим на примере как этим пользоваться и каков будет результат. Для этого создадим нашу собственную структуру Test и один ее экземпляр.

struct Test {
  var x: Int
}
let val = Test(x: 5)
// Ну и используем наш Box
var box = Box(val)
var box2 = box
address(&box.value) 
address(&box2.value)

Как и планировалось, при выводе мы получим один и тот же адрес:

address: 140701847686488
address: 140701847686488

Источники

  • Understanding Swift Copy-on-write mechanism

  • What is copy on write?

  • Copy-on-write in Swift

Что не так с Copy-on-Write под Linux при копировании / Хабр

Предупреждение: эта статья относится ко всем CoW файловым системам в Linux, поддерживающим reflink при копировании. В данный момент это: BTRFS, XFS и OCFS2.

Прошу воздержаться от холиваров о том, какая ФС лучше: Btrfs, XFS, Reiser4, NILFS2, ZFS или какая-то неупомянутая.

Предыстория

  • 21 июля 2001 года — Namesys публикует анонс Reiser4. DARPA спонсирует разработку.
  • 20 ноября 2003 — Namesys публикует некоторые бенчмарки Reiser4.
  • 24 августа 2004 — Namesys делает публичный релиз Reiser4.
  • 14 сентября 2004 — анонс ZFS.
  • 16 ноября 2005 — ZFS включена в 27 сборку OpenSolaris.
  • сентябрь 2006 — Ханс Райзер арестован за убийство жены, Нины Райзер. Начало конца Namesys
  • 12 июня 2007 — анонс Btrfs Крисом Мейсоном (бывший сотрудник Namesys).
  • 22 сентября 2011 — В ZFSonLinux появился тикет с запросом на реализацию reflink.
  • 2012 — Btrfs признана стабильной Oracle Linux и SUSE Linux Enterprise
  • 21 января 2013 — метка экспериментальности была снята c Btrfs в исходных кодах ядра Linux.
  • май 2019 — Btrfs удалена из RHEL 8 (подозревается скрытые политические причины, так как Btrfs продвигается Oracle)

Ожидания от копирования в CoW-файловых системах

Когда в 2001 году анонсировалась Reiser4, я был вдохновлён и увлечён возможностями Copy-on-Write. Подумать только, мы можем легко и просто иметь сколько угодно копий разных проектов, а физически диск будет хранить только отличия между ними!

К тому же скорость копирования должна была неприлично вырасти. За счёт того, что при копировании создавалась бы только reflink-ссылка на старый файл. При записи в такой новый файл автоматически выделялись бы сектора для изменённых данных. В итоге мы бы имели одинаковые сектора для общих частей файлов, а разные части были бы записаны в разных секторах.

Это тогда выглядело панацеей для создания аккаунтов для shared-хостинга, а сейчас оптимальным решением для лёгких виртуальных машин и контейнеров. Ведь мы могли бы не тратить место на одинаковые файлы, вместе с тем позволяя пользователям их беспроблемно менять.

Однако у Ханса Райзера поехала крыша и он убил жену, а его детище (скорее всего по политическим причинам) не включили в ядро. Может всё-таки история зависит от конкретной личности?

ZFS была опубликована под несовместимой с Linux лицензией, и потому не вошла в ядро. Поэтому внедрение ZFS в Linux оказалось надолго заторможено.

После этого я стал ждать Btrfs. И только через 6 лет после анонса она была признана разработчиками ядра Linux стабильной.

После этого я не торопился использовать Cow-системы, так как сама парадигма Copy-on-Write предполагает повышенную фрагментацию, потому что изменения данных каждый раз записываются в новое место.

Для HDD фрагментация убивает производительность, так как процесс перепозиционирования блока считывающих головок — очень длительная операция.

Поэтому лично я откладывал внедрение btrfs на своих машинах, пока они не перешли на SSD.

Замечу, что SSD тоже не любят фрагментацию, всем известно, что для SSD линейная запись/чтение могут быть в десятки раз быстрее, чем случайный доступ.

Но производительность фрагментированного SSD падает не так драматически, как у HDD.

Итак, что со скоростью копирования при CoW?

И вот, наконец, настал тот час. Когда SSD стали достаточно надёжны, я стал вовсю использовать CoW-файловые системы. А конкретнее — Btrfs и Nilfs2.

Осваивая захватывающие возможности снимков (снепшотов), на время я забыл о моих ожиданиях из 2000-х годов о сверхбыстром копировании файлов.

Через некоторое время я решил провести тесты. И к моему великому разочарованию не увидел никакого прироста скорости от CoW при копировании. Вот результат на SATA SSD:

time cp -a /usr /usr1
real    0m15,572s
user    0m0,240s
sys     0m4,739s

Оказалось, что для использования CoW нужно указывать специальный ключ.

time cp -a --reflink=auto /usr /usr2
real    0m3,166s
user    0m0,178s
sys     0m2,891s

Только в этом случае мы видим 5-кратное преимущество.

К слову говоря, размер преимущества неограниченно растёт при увеличении длины файлов. В папке /usr, которую я копировал, в основном мелкие файлы.

Я был несказанно удивлён, почему ключевое преимущество CoW-файловой системы не используется по умолчанию. Ведь для этого она и создавалась!

В данном случае я тестировал копирование на Btrfs-разделе. Но подобный результат вы получите и с любой другой CoW-файловой системой, поддерживающей reflink.

В чём проблема, Билли?

Проблема в cp. По умолчанию она не использует CoW при копировании. Хотя может.

Вы можете сказать — я не использую cp. Однако под капотом у Linux он используется практически везде. Многие программы, когда им нужно что-то скопировать, используют cp, а не свои «велосипеды».

Почему разработчиками coreutils было принято столь неоднозначное решение, которое перечеркнуло половину преимуществ CoW файловых систем?

Оказывается, так решил Pádraig Brady, ответственный за развитие GNU coreutils.

Вот его логика:

  1. По умолчанию cp не использует CoW, так как кто-то может использовать копирование для того, чтобы повысить вероятность сохранения файла на диске после разрушения файловой системы.
  2. С точки зрения производительности, если есть некий чувствительный к задержкам процесс, вы можете захотеть, чтобы основная запись была сделана именно во время копирования, так если это произойдёт потом, то может возникнуть лаг при перепозиционировании головок жёсткого диска. Обратите внимание, начиная с версии 8.24 coreutils, mv по умолчанию использует опцию reflink.

Текст ответа Pádraig Brady на английском

It’s not the default since for robustness reasons one may want a copy to take place to protect against data corruption. Also for performance reasons you may want the writes to happen at copy time rather than some latency sensitive process working on a CoW file and being delayed by the writes possibly to a different part of a mechanical disk. Note that from coreutils v8.24 mv will reflink by default, since it doesn’t have the above constraints.

В плане скорости для mv практически нет никакой пользы от CoW при перемещении файлов. В пределах одной файловой системы mv практически всегда работает очень быстро.

Опять возникает вопрос о влиянии личности на историю. Поставив по-другому несколько буковок в исходном коде программы, можно замедлить копирование у десятков/сотен миллионов пользователей (тут нужно учесть, что сейчас большинство людей пользуются облачными сервисами в том или ином виде, даже если у них нет компьютера), уменьшить эффективность использования накопителей и повысить их продажи по всему миру.

Разбор аргументов Pádraig

В первом аргументе есть кое-какой смысл. Неискушенные пользователи действительно могут делать бэкапы ценных файлов на одной и той же файловой системе.

Второй аргумент. Если почитать дальнейшие комментарии насчёт лагов от Pádraig, то обнаружится, что он имел в виду ситуации с базами данных, когда при записи в существующий файл может возникнуть задержка из-за того, что файловая система будет искать свободное место. Но в случае CoW-файловой системы всегда будут искаться новые сектора для записи в силу природы CoW, как заметил Jan Kanis. Поэтому, по моему мнению, второй аргумент несостоятелен.

Однако на CoW системах действительно можно получить задержку или даже ошибку “Закончилось пространство” при записи в файл базы данных. Для избежания этого нужно для базы изначально создавать пустой каталог с отключенным CoW.

Отключается CoW на каталоге/файле так:

chattr +C /dir/file
chattr +C /dir/dir

Есть ещё опция монтирования nodatacow. Она будет применена ко всем новосоздаваемым файлам.

Как всё-таки быть, если мы хотим по умолчанию использовать CoW при копировании?

  1. Радикальный путь — это патчить coreutils. Возможно, создать свой пакет в своём частном репозитории.

    Патчим файл cp.c:

    -x->reflink_mode = REFLINK_NEVER;
    +x->reflink_mode = REFLINK_AUTO;
  2. Менее радикальное решение — это прописать алиас cp для вашей оболочки. Для bash, например:

    В папке /etc/profile.d создаём файлик cp_reflink.sh c содержимым:

    #!/bin/bash
    alias cp='cp --reflink=auto'

    Это решение будет работать почти во всех случаях, когда к cp идёт обращение из оболочки по имени. Но если в скриптах будет использоваться /bin/cp, то алиас не сработает и копирование будет происходить по старинке.

Дистрибутивы Linux c поддержкой reflink

Состояние на 18 января 2021 года:

  • Fedora 33 c пакетом coreutils-8.32-11.fc33. Детали от justaguest

Файловые менеджеры и reflink

Состояние на 31 октября 2019 года:

  • Midnight Commander — поддерживает.
  • Krusader — не поддерживает.
  • Dolphin — не поддерживает.
  • Nautilus — не поддерживает.
  • Nemo — поддерживает.

По слухам на 18 января 2021 года:

  • Dolphin и Nautilus уже поддерживают копирование с reflink. Точные минимальные версии с поддержкой reflink автору не известны.

Языки программирования, системные вызовы и reflink

В большинстве языков программирования поддержка reflink отсутствует.
На Си многие программисты до сих пор копируют с помощью циклов и буферов.

Системный вызов sendfile не использует reflink.
cp использует системный вызов ioctl с флагом FICLONE.

Я думаю, если нужно в коде что-то скопировать, то желательно делать это как делает cp или просто вызвать cp --reflink=auto.

Выводы

С наступлением эпохи повсеместной виртуализации и SSD стало очень актуально использовать CoW-файловые системы. Они удобны для создания снимков, быстрого копирования. Фактически, при использовании CoW при копировании мы автоматически делаем дедупликацию данных.

Сейчас только 3 файловых системы поддерживают этот тип копирования: BTRFS, XFS и OCFS2.

Я искренне надеюсь, что поддержку reflink допилят в ZFS и NILFS2, так как по внутренним механизмам они и так поддерживают CoW.

Однако во всех дистрибутивах Linux CoW при копировании файлов отключена, и нам нужно или явно указывать соответствующие ключи, или использовать различные трюки типа алиасов или патчей.

С момента анонса Reiser4 прошло уже 18 лет, однако до сих пор лёгкое CoW копирование не вошло в нашу жизнь повсеместно.

P.S. Docker и CoW

А вы знаете что Docker поддерживает btrfs для своего хранилища? Эту опцию нужно включать. Она не выбрана по умолчанию.

CoW файловая система в теории является идеальным дополнением к лёгкой виртуализации, когда разные виртуальные машины используют одно и то же ядро.

На мой взгляд, гораздо более органично, чем OverlayFS и Aufs, которые представляют из себя технологические костыли для имитации CoW.

Для использования Btrfs в Docker нужно:

  1. В свежей (без образов и виртуальных машин) инсталляции Docker примонтировать отдельный том Btrfs к /var/lib/docker
  2. Добавить опции в конфигурационный файл запуска Docker

Для Gentoo и Calculate Linux это /etc/conf. d/docker

DOCKER_OPTS="--storage-driver btrfs --data-root /var/lib/docker"

А вот полная инструкция для всех дистрибутивов.

Дополнения из комментариев


XFS и CoW

constb (источник): На XFS тоже не всегда поддерживается CoW. Нужно создавать файловую систему c mkfs.xfs -m reflink=1.
Уже на созданной ФС «включить» поддержку CoW нельзя. плюс, на ядрах до 4.11 включение этой опции вызывает красные варнинги в dmesg о том что фича – экспериментальная.

MacOS и CoW

MMik (источник): В OS X подобная опция команды cp – это -c.

Она включает использование атомарного системного вызова clonefile(), который создаёт запись в файловой системе о новом файле, в котором ссылки на блоки данных идентичны оригиналу. Копируются атрибуты и расширенные атрибуты файла, записи списков контроля доступа (ACL), за исключением информации о владельце файла и со сбросом setuid/setgid битов. Работает в пределах одной файловой системы, требует поддержки файловой системой (атрибут ATTR_VOL_CAPABILITIES, флаг VOL_CAP_INT_CLONE).

Поддерживается начиная с OS X 10.12 (Sierra) и только в файловой системе APFS.


Благодарности:

  • Компании RUVDS за поддержку и возможность публикации в своем блоге на Хабре.
  • За изображение TripletConcept.

P.P.S. Замеченные ошибки направляйте в личку. Повышаю за это карму.


Вы можете поэкспериментировать с CoW-файловыми системами, заказав виртуальную машину у RUVDS по купону ниже.

Оптимизация

— что такое копирование при записи?

Книга Design Patterns: Elements of Reusable Object-Oriented Software Erich Gamma et al. ясно описывает оптимизацию копирования при записи (раздел «Последствия», глава «Прокси»):

Шаблон прокси вводит уровень косвенности при доступе к объект. Дополнительная косвенность имеет множество применений, в зависимости от вид прокси:

  1. Удаленный прокси может скрыть тот факт, что объект находится в другом адресном пространстве.
  2. Виртуальный прокси может выполнять оптимизацию, например создавать объект по требованию.
  3. Как прокси-серверы защиты, так и смарт-ссылки позволяют выполнять дополнительные вспомогательные задачи при доступе к объекту.

Есть еще одна оптимизация, которую шаблон Proxy может скрыть от клиент. Он называется копирование при записи и связан с созданием на требование. Копирование большого и сложного объекта может быть дорогостоящим операция. Если копия никогда не изменялась, то нет необходимости нести эту стоимость. Используя прокси для отсрочки процесса копирования, мы убедиться, что мы платим цену за копирование объекта только в том случае, если он модифицированный.

Чтобы копирование при записи работало, ссылка на субъект должна учитываться. Копирование прокси не сделает ничего, кроме увеличения этой ссылки. считать. Только когда клиент запрашивает операцию, которая изменяет Тема действительно ли прокси копирует его. В этом случае прокси должен также уменьшить счетчик ссылок субъекта. Когда счетчик ссылок становится равным нулю, тема удаляется.

Копирование при записи может снизить стоимость копирования тяжеловесных объектов существенно.

Ниже приведена реализация Python оптимизации копирования при записи с использованием шаблона Proxy. Целью этого шаблона проектирования является предоставление суррогата другому объекту для управления доступом к нему.

Диаграмма классов шаблона Proxy:

Диаграмма объектов шаблона Proxy:

Сначала мы определяем интерфейс субъекта:

 import abc
Тема класса (abc.ABC):
    @abc.abstractmethod
    клон защиты (я):
        поднять NotImplementedError
    @abc.abstractmethod
    деф читать (сам):
        поднять NotImplementedError
    @abc. abstractmethod
    Запись по определению (я, данные):
        поднять NotImplementedError
 

Далее мы определяем реальный субъект, реализующий интерфейс субъекта:

 импортировать копию
класс RealSubject(Тема):
    def __init__(я, данные):
        self.data = данные
    клон защиты (я):
        вернуть копию.deepcopy(я)
    деф читать (сам):
        вернуть self.data
    Запись по определению (я, данные):
        self.data = данные
 

Наконец, мы определяем прокси, реализующий интерфейс субъекта и ссылающийся на реальный субъект:

 класс Прокси(Тема):
    def __init__(я, субъект):
        self.subject = тема
        пытаться:
            self.subject.counter += 1
        кроме AttributeError:
            self.subject.counter = 1
    клон защиты (я):
        return Proxy(self.subject) # совместное использование атрибутов (поверхностная копия)
    деф читать (сам):
        вернуть self.subject.read()
    Запись по определению (я, данные):
        если self. subject.counter > 1:
            self.subject.counter -= 1
            self.subject = self.subject.clone() # копирование атрибута (глубокое копирование)
            self.subject.counter = 1
        self.subject.write(данные)
 

Затем клиент может извлечь выгоду из оптимизации копирования при записи, используя прокси-сервер в качестве замены для реального субъекта:

 if __name__ == '__main__':
    x = Прокси (RealSubject ('foo'))
    x.write('бар')
    y = x.clone() # реальная тема используется совместно, а не копируется
    print(x.read(), y.read()) # полоса полоса
    утверждать, что x.subject является y.subject
    x.write('baz') # реальная тема копируется при записи, потому что она была опубликована
    print(x.read(), y.read()) # базовая панель
    утверждать, что x.subject не является y.subject
 

Стратегия Docker «копирование при записи (CoW)» — переполнение стека Стратегия копирования при записи

(CoW) Я знаю, что это означает в целом, но

1: Что это означает в Docker ?

2: Почему он используется в Docker ?

  • докер

0

стратегия копирования при записи (CoW)

  • Большинство объединенных файловых систем используют то, что называется копированием при записи, что легче понять, если рассматривать его как копирование при изменении.

  • При изменении файла в слое только для чтения (не в верхнем слое) весь файл сначала копируется из слоя только для чтения в доступный для записи слой перед внесением изменений.

  • Это негативно влияет на производительность среды выполнения и размер образа.

Что это значит в Docker?

Давайте возьмем образ ubuntu:latest и проделаем над ним пару операций.

Уровень 1:

 docker container run --name mod_ubuntu ubuntu: последнее касание /mychange
docker container diff mod_ubuntu # чтобы получить отличия от базового образа
 

Эта команда выдаст вывод:

 A /mychange
 

Уровень 2:

 docker container run --name mod_busybox_delete busybox:latest rm /etc/passwd
diff-контейнер docker mod_busybox_delete
 

На этот раз вывод будет состоять из двух строк:

 C /etc
D /etc/passwd
 

Уровень 3:

 запуск контейнера докеров --name mod_busybox_change busybox:последнее касание /etc/passwd
Разница между контейнерами докеров mod_busybox_change
 

Подкоманда diff покажет два изменения:

 C /etc
C /etc/passwd
 

Это не точное изображение (отсутствует операция удаления).

Разное

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *