write-through
캐시에 쓸 때 원본에도 함께 쓰는 방식입니다. 양쪽에 다 닿아야 그 쓰기가 끝난 것으로 봅니다. 그래서 캐시에는 원본에 아직 못 간 내용이 남지 않습니다. 캐시를 굴리는 쪽이 고르는 정책이지 캐시와 원본이 서로 합의하는 규칙이 아닙니다.
상세
쓰기를 어디까지 밀고 나서 완료라 부를지가 캐시 쓰기 정책을 가릅니다. write-through 는 그 선을 원본까지로 잡습니다. 리눅스 커널의 블록 계층 캐시에서 writethrough 를 고르면, 캐시된 블록에 대한 쓰기는 오리진 장치와 캐시 장치 양쪽에 닿기 전에는 완료되지 않습니다. 그리고 깨끗한 블록은 깨끗한 채로 남아야 합니다.
sequenceDiagram
autonumber
participant 앱
participant 캐시
participant 원본
앱->>캐시: 쓰기
앱->>원본: 같은 쓰기
캐시-->>앱: 닿음
원본-->>앱: 닿음
Note over 캐시: 깨끗한 채로 남는다
같은 쓰기가 두 자리에 들어갑니다. 둘 중 하나만 닿은 상태는 완료가 아닙니다. 그래서 캐시에
원본과 다른 내용이 얹혀 있는 구간이 생기지 않습니다. 같은 계층의 인자 설명은 이것을 더 짧게
못 박습니다. writethrough 인자는 캐시 블록의 내용이 오리진 블록의 내용과 달라지는 것을
금지하는 write through 캐싱이라고 적혀 있습니다. 이 인자를 안 주면 기본 동작은 캐시 블록의
내용을 나중에 원본으로 되쓰는 것이고, 그때는 둘이 달라질 수 있습니다.
기본값 쪽과 견주면 선이 더 뚜렷해집니다. 그 계층의 동작 모드 셋은 writeback · writethrough · passthrough 입니다. 기본값인 writeback 을 고르면 캐시된 블록에 대한 쓰기는 캐시로만 가고 그 블록은 메타데이터에서 dirty 로 표시됩니다. write-through 에는 그 dirty 표시가 생기지 않습니다. 되쓸 것이 없기 때문입니다.
애플리케이션 캐시에서도 완료의 선은 같습니다. 다만 원본으로 가는 그 쓰기를 누가 내보내는지가 갈립니다. 한쪽은 애플리케이션이 두 곳에 각각 씁니다. 데이터베이스에 데이터를 쓸 때마다 캐시에 데이터를 더하거나 갱신하는 것이 write-through 전략이라는 정의입니다. 쓰기를 하는 쪽이 캐시와 원본 둘을 함께 움직입니다. 캐시가 채워지는 자리도 쓰기 경로입니다. 읽다가 캐시 미스가 나서 채우는 cache-aside 나 lazy loading 과 갈리는 자리가 여기입니다.
다른 쪽은 캐시가 원본으로 보냅니다. 애플리케이션은 캐시에만 쓰고, 캐시에 붙은 writer 부품이 그
쓰기를 시스템 오브 레코드까지 통과시킵니다. read-through 와 write-through 를
CacheLoaderWriter 라는 인터페이스 하나 뒤에 합쳐 두고 메서드를 두 묶음으로 가르는 구현이 그
꼴입니다. load(K) 와 loadAll(Iterable) 이 read-through 쪽이고, write(K, V) ·
writeAll(Iterable) · delete(K) · deleteAll(Iterable) 이 write-through 쪽입니다. 캐시에
쓰는 호출이 시스템 오브 레코드까지 통과하는 것이 write-through 라는 뜻입니다. 두 정의가 가리키는
선은 같고, 다른 것은 그 두 번째 쓰기를 애플리케이션이 내보내나 캐시가 내보내나입니다.
이름은 한 계층의 것이 아닙니다. CPU(Central Processing Unit, 중앙 처리 장치) 의 메모리 타입 이름표, 블록 계층 캐시의 설정값, 애플리케이션 캐시의 전략 이름에서 같은 이름이 같은 뜻으로 되풀이됩니다.
대가
얻는 것
- 캐시의 데이터가 낡지 않습니다. 데이터베이스에 쓸 때마다 캐시도 갱신되므로 캐시에 있는 데이터는 늘 현재 값입니다.
- 캐시와 원본이 달라지는 구간이 없습니다. 쓰기가 양쪽에 닿기 전에는 완료가 아니고, 되쓸 것이 없으니 dirty 표시도 생기지 않습니다.
내주는 것
- 쓰기 한 번이 두 번의 이동이 됩니다. 모든 쓰기가 캐시에 한 번, 데이터베이스에 한 번 두 번을 오가며 그것이 과정에 지연을 더합니다. 원본이 응답할 때까지 그 쓰기는 안 끝난 것이므로, 쓰기 지연은 원본 매체가 정합니다.
- 쓰기를 모아서 나중에 처리할 여지를 내줍니다. 블록 계층의 기본 동작은 캐시 블록의 내용을 나중에 되쓰는 것이고 그 이유가 성능입니다. write-through 인자는 그 미루기를 금지합니다.
- 읽히지 않을 데이터가 캐시로 들어옵니다. 이것을 cache churn 이라 부릅니다. 대부분의 데이터는 한 번도 읽히지 않으므로 그만큼이 자원의 낭비입니다. 쓰기가 캐시를 채우는 구조라서 캐시의 내용이 읽기 수요가 아니라 쓰기 수요를 따라갑니다.
- 새로 띄운 노드는 비어 있습니다. 노드 장애 때문이든 확장 때문이든 새 노드를 띄우면 데이터가 없습니다. 그 데이터는 데이터베이스에서 다시 쓰이거나 갱신될 때까지 계속 없습니다. lazy loading 을 write-through 와 함께 구현하면 이것을 줄일 수 있습니다.
예시
dm-cache 의 테이블 인자
cache <metadata dev> <cache dev> <origin dev> <block size> <#feature args> [<feature arg>]* <policy> <#policy args> [policy args]*
디바이스 매퍼 타깃을 만드는 줄입니다. <#feature args> 는 뒤에 몇 개의 기능 인자를 넘기는지
적는 자리이고, 그 뒤 <feature arg> 자리에 들어갈 수 있는 값이 writethrough 또는
passthrough 입니다. 커널 문서는 기본값이 writeback 이라고 괄호로 적어 둡니다. 즉 이 인자를
한 개 넣는 것이 write-through 를 켜는 최소 형태입니다. <metadata dev> 는 영속 메타데이터를
담고 <cache dev> 는 캐시된 데이터 블록을 담습니다. 커널 문서는 이 둘을 원본 데이터 블록을 담는 <origin dev> 보다 빠른 장치로 적습니다.
bcache 의 cache_mode
/sys/block/<bdev>/bcache/cache_mode
블록 계층 캐시 bcache 는 백킹 장치마다 sysfs 파일로 모드를 노출합니다. 자리는
/sys/block/<bdev>/bcache 와 /sys/block/bcache*/bcache 이고, 붙어 있으면
/sys/fs/bcache/<cset-uuid>/bdev* 에도 있습니다. cache_mode 가 가질 수 있는 값은
writethrough · writeback · writearound · none 넷입니다. 커널 문서는 writethrough 와
writeback 캐싱을 둘 다 지원하고 writeback 은 기본이 꺼짐이며 런타임에 임의로 켜고 끌 수 있다고
적습니다.
x86 메모리 타입 WT
wb write-back
uc uncached
wc write-combined
wt write-through
uc- uncached minus
PAT(Page Attribute Table, 페이지 속성 테이블) 문서가 싣고 있는 메모리 속성 이름표입니다. PAT 는 페이지 단위로 메모리 속성을 정하게 해 주고, 이 다섯이 그중 흔히 쓰이는 값입니다. 캐시 정책의 이름이 CPU 가 읽는 페이지 속성으로 그대로 내려와 있는 자리입니다. 그보다 오래된 MTRR(Memory Type Range Register, 메모리 타입 범위 레지스터) 쪽에도 같은 이름이 남아 있습니다.
static char *mtrr_strings[MTRR_NUM_TYPES] = { "uncachable", "write-combining", "?", "?", "write-through", "write-protect", "write-back", };
MTRR 은 물리 주소 구간 단위로 메모리 타입을 정하는 옛 방식이고, 커널 문서는 현대 x86 하드웨어 에서는 이 쓰임이 PAT 로 대체됐다고 적습니다.
Ehcache 의 CacheLoaderWriter 등록
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(true);
Cache<Long, String> writeThroughCache = cacheManager.createCache("writeThroughCache",
CacheConfigurationBuilder.newCacheConfigurationBuilder(Long.class, String.class, ResourcePoolsBuilder.heap(10))
.withLoaderWriter(new SampleLoaderWriter<>(singletonMap(41L, "zero")))
.build());
assertThat(writeThroughCache.get(41L), is("zero"));
writeThroughCache.put(42L, "one");
assertThat(writeThroughCache.get(42L), equalTo("one"));
cacheManager.close();
withLoaderWriter 로 CacheLoaderWriter 를 캐시에 등록합니다. 등록한 것은 41L 이 "zero"
에 대응한다는 매핑을 아는 샘플입니다. 캐시에는 아직 내용이 없으므로 get(41L) 은
CacheLoaderWriter 에 위임되고, 돌아온 매핑이 캐시를 채우면서 호출한 쪽으로도 돌아갑니다.
여기까지가 read-through 쪽입니다. put(42L, "one") 으로 캐시 매핑을 만들 때는
CacheLoaderWriter 가 불려 그 매핑을 시스템 오브 레코드에 씁니다. 이 절반이 write-through
입니다.
실패
깨지는 자리는 캐시 장치 쪽입니다. 원본에는 같은 값이 이미 갔거나 함께 가고 있으므로, 캐시 쪽 사고가 데이터 자체를 없애지는 않습니다.
flowchart TD
A["writethrough 쓰기"] --> B{"캐시 쓰기가 에러인가"}
B -->|아니오| C["양쪽에 닿음. 완료"]
B -->|예| D["그 LBA 의 캐시 데이터를 무효화"]
D --> E["캐시를 우회한 쓰기와 같은 처리"]
bcache 문서는 캐시 장치로 오가는 입출력 에러를 정상 동작에 영향을 주지 않고 처리하려 한다고 적습니다. writethrough 쓰기에서 캐시 쪽 쓰기가 에러를 내면 그 LBA(Logical Block Address, 논리 블록 주소)의 캐시 데이터를 무효화하는 쪽으로 바꿉니다. 캐시를 우회하는 쓰기에 하는 것과 같은 처리입니다. writeback 쓰기는 다릅니다. 그 에러를 파일시스템과 유저스페이스로 그대로 올려보낸다고 적혀 있습니다.
| 조건 | 그때 벌어지는 일 |
|---|---|
| 캐시 쪽 쓰기가 에러를 낸다 | 그 LBA 의 캐시 데이터를 무효화하고 넘어갑니다. 캐시를 우회한 쓰기와 같은 처리입니다 |
| 캐시 장치의 에러가 임계를 넘는다 | bcache 가 캐시 장치를 내리고 붙어 있던 백킹 장치를 전부 passthrough 모드로 돌립니다. 임계는 설정할 수 있고 기본값은 0 입니다 |
| 캐시 장치를 통째로 잃는다 | 캐시가 writethrough 모드면 캐시 장치를 그냥 버려도 데이터를 잃지 않습니다 |
| 캐시가 dirty 인 채로 캐시용 볼륨을 줄인다 | write-through 는 이 자리에 안 걸립니다. dm-cache 문서는 writethrough 와 passthrough 모드가 이미 깨끗한 캐시를 유지한다고 적습니다 |
커널이 없는 자리에서 데이터를 되찾는 절차도 같은 결을 보입니다. bcache 문서는 백킹 장치의
파일시스템이 8KiB 오프셋에 그대로 있으므로 losetup -o 8192 로 루프 장치를 만들어 열 수 있다고
적습니다. 그리고 캐시가 writethrough 모드면 캐시 장치를 안전하게 버려도 데이터를 잃지 않는다고
덧붙입니다.
운영
모드를 켜고 끄는 손잡이는 계층마다 이름이 다릅니다. bcache 는 sysfs 의 cache_mode 파일이고,
값은 writethrough · writeback · writearound · none 입니다. writeback 은 기본이
꺼짐이고 런타임에 임의로 켜고 끌 수 있습니다. dm-cache 는 타깃을 만드는 줄의 기능 인자
자리에서 정합니다.
모드를 옮기는 자리에는 조건이 붙습니다. dm-cache 문서는 passthrough 를 캐시 내용이 오리진
장치와 일관되다고 알려지지 않았을 때 쓸모 있다고 적습니다. passthrough 에서는 모든 읽기가 오리진
장치에서 처리되고 모든 쓰기가 오리진 장치로 넘어가며, 쓰기 적중은 캐시 블록 무효화를 일으킵니다.
passthrough 모드를 켜려면 캐시가 깨끗해야 합니다. 그 뒤에 일관성이 확인되거나
invalidate_cblocks 메시지로 세워지면, 캐시가 아직 warm 인 채로 writethrough 나 writeback 으로
옮길 수 있습니다.
줄이기 전에 볼 것이 있습니다. dm-cache 문서는 캐시용 장치로 쓰는 볼륨을 캐시가 깨끗해지기 전에는 절대 줄이지 말라고 적습니다. writeback 모드를 쓸 때 특히 중요하다고 덧붙이고, writethrough 와 passthrough 모드는 이미 깨끗한 캐시를 유지한다고 적습니다.
애플리케이션 캐시 쪽 손잡이는 TTL(Time To Live, 생존 시간)입니다. Amazon ElastiCache 문서는 각 쓰기에 TTL 값을 붙이면 두 전략의 이점을 함께 가질 수 있다고 적습니다. lazy loading 은 낡은 데이터를 허용하지만 빈 노드에서 실패하지 않고, write-through 는 데이터가 늘 새것임을 보장하지만 빈 노드에서 실패할 수 있고 군더더기 데이터로 캐시를 채울 수 있다는 것이 그 이유입니다.
관련 항목
이것을 실제로 구현·채택한 제품
dm-cache · bcache · Ehcache · Amazon ElastiCache · CacheLoaderWriter
이것을 대신할 수 있는 다른 수단
cache-aside · read-through · write-behind · passthrough · writearound
이 이름이 그대로 쓰이는 CPU 캐시 계층
이것이 지키는 성질과 원칙
write policy · 캐싱 · no-write allocate · dirty · 무효화
이것이 치르는 대가
이것이 쓰기를 미는 저장소
굴릴 때 조정하는 운영 손잡이
다른 이름: write through · writethrough · 라이트 스루