본 포스팅은 데이터 중심 애플리케이션 설계의 7장 트랜잭션 단원을 참조하여 작성되었습니다.

또한 포스팅 내용 중 개인적인 의견이 들어간 부분도 있으니 자세한 내용은 책을 참조해주세요.

 

아무리 대단한 데이터 시스템이라도 언제나 아래와 같은 문제 상황이 발생할 수 있다.

- 데이터베이스의 소프트웨어 또는 하드웨어의 실패

- 예상치 못한 애플리케이션의 종료

- 네트워크 단절

- 여러 클라이언트의 동시 쓰기에 의한 덮어쓰기

- 데이터의 부분 갱신

- 클라이언트의 Race Condition에 의한 버그

 

뛰어난 시스템은 위와 같은 결함을 처리해서 전체적으로 시스템 장애로 이어지는 것을 막아야 한다.

이러한 결함을 다루는 기술은 트랜잭션이라는 매커니즘으로 사용되어 왔다.

 

트랜잭션은 애플리케이션의 몇 개의 읽기와 쓰기를 하나의 논리적 단위로 묶는 방법이다.

Transaction

Application에서 발생한 많은 명령을 하나의 Transaction으로 묶어 Database에 전달하고

Database는 Transaction의 모든 명령이 실행될 수 있는지 확인하고 성공한다면 Commit을 진행하고

실패한다면 기존에 실행된 Transaction 명령을 Rollback하고 Application에 Abort를 반환한다.

 

이는 Applicatoin의 Database에서 발생할 수 있는 다양한 문제를 Transaction이라는 개념을 통해 단순화 한 것이다.

Transaction은 수치적으로 정해진 것이 아니기 때문에 각 Application의 특성에 맞게 조절하는 것이 중요하다.

 

그렇다면 Application이 어느정도의 Transaction Level이 필요한지 어떻게 확인할 수 있을까?

이를 위해선 Transaction Level에 따른 제공 기능을 확인해보고 결정할 수 있을 것이다.

주로 연관된 문제는 동시성 문제이며 일반적으로 Database는 아래의 3가지 정도의 Transaction Level을 제공한다.

 

  • Read committed (커밋 후 읽기)
  • Snapshot Isolation (스냅샷 격리)
  • Serializability (직렬성)

Transaction을 지원하는 다양한 Database는 ACID라는 조건을 만족한다.

여기서 ACID는 원자성(Atomicity), 일관성(Consistency), 격리성(Isolation), 지속성(Duration)을 의미한다.

 

원자성이란 위에서 봤던 여러 명령을 가진 Transaction이 성공하면 commit되고 실패하면 전체가 rollback되는 것을 말한다.

즉, 하나의 Transaction은 쪼갤 수 없는 단위가 된다.

일관성은 데이터는 항상 진실이어야 한다는 개념이다.

다소 모호한 개념인데, 이는 Application에서 의미하는 데이터의 성질과 관계에 따른 일관성 보존을 의미한다.

예를들어 계좌 송금 시스템에서 송금된 금액은 반드시 목적지 계좌에 추가되며 송금된 계좌에서는 금액이 차감되는 일관성이 유지되어야 한다.

격리성은 동시에 실행되는 Transaction은 서로 격리되어야 하며 서로를 방해할 수 없다를 의미한다.

격리성을 보장하기 위한 방법은 다양한데 이는 Transaction Level에 따라 다르게 구현된다.

지속성은 Database에 저장된 데이터는 손실되지 않는 안전한 곳에 저장되어야 한다는 의미다.

 

ACID의 성질 중 Transaction과 직접적으로 연관되어 있는 것은 원자성과 격리성이다.

따라서 이 2개의 성질에 대해서 보장하는 방식을 알아보도록 하자.

 

문제점 1 - Dirty Read

특정 시간에 오픈되는 공연 티켓 예매 서비스가 있다고 생각해보자.

사용자는 관람하고 싶은 좌석 등급을 고르고 세부 좌석을 선택하고 금액을 지불해서 예매를 진행한다.

티켓 예매가 오픈되면 순간적인 트래픽이 급증하기 때문에 등급마다 남은 좌석 수를 별개의 테이블에 저장하고

사용자가 좌석 등급을 고를 때 해당 테이블에서 좌석 수를 읽어 보여준다.

 

그러면 특정 사용자가 좌석 예매를 완료했다고 하면 발생해야 하는 UPDATE는 2개가 된다.

UPDATE seats SET booked = true WHERE grade=S AND row=10 AND col=10
UPDATE seats_count SET count = count+1 WHERE grade=S

 

만약 첫번째 UPDATE가 실행되고 두번째 UPDATE가 실행되기 전에 다른 사용자가 S 등급의 좌석 수 조회를 요청한다면

실제 예매 가능한 좌석수와 조회되는 좌석 수는 다르게 나올 것이다.

이러한 현상을 격리성 위반, Dirty Read라고 한다.

즉, UPDATE를 2개 실행해야 하는 트랜잭션 A가 완료되기 전에 조회하는 트랜잭션 B가 커밋되지 않은 트랜잭션 A의 중간 결과를 읽을 수 있게 된 것이다.

 

문제점 2 - Dirty Write

두 사용자가 동일한 좌석을 예매하고 금액을 지불할 경우를 생각해보자.

# A 사용자
UPDATE seats SET booked = true WHERE grade=S AND row=10 AND col=10 AND booked=false # 1
UPDATE payment SET buyer='A' WHERE grade=S AND row=10 AND col=10 # 2

# B 사용자
UPDATE seats SET booked = true WHERE grade=S AND row=10 AND col=10 AND booked=false # 3
UPDATE payment SET buyer='B' WHERE grade=S AND row=10 AND col=10 # 4

 

 

굉장히 드문 경우겠지만, 두 사용자가 동시에 UPDATE seats를 했다면 두 사용자 모두 금액을 지불 할 수 있게 된다.

이렇게 되면 마지막에 금액을 지불한 사용자가 좌석의 buyer가 된다.

위처럼 seats table을 갱신하기 전에 다른 사용자 역시 갱신 요청을 하게 된다면 큰 문제를 유발시킬 수 있다.

문제 발생의 원인은 예매하려는 좌석에 대해 잠금을 하지 못해서 발생한 것으로 Dirty Write 문제라고 한다.

 

해결 방법 1 - Read Committed (커밋 후 읽기)

Drity Write  문제는 사실 MySQL과 같은 일반적인 RDBMS를 사용한다면 거의 겪지 않을 것이다.

RDBMS는 기본적으로 데이터베이스의 Row 수준의 잠금을 지원하기 때문이다.

따라서 트랜잭션이 특정 Row를 변경하고 싶다면 트랜잭션은 잠금을 획득해야하고 commit 또는 abort가 일어난 후에 다른 트랜잭션에서 해당 Row를 변경할 수 있게 된다.

그렇다면 Dirty Read의 경우는 어떨까?

어떤 트랜잭션이 변경을 위해 Row 잠금을 획득한 상태라면, 해당 Row를 읽기를 원하는 트랜잭션은 변경이 끝날 때까지 기다려야 할까?

대부분의 데이터베이스는 변경이 끝날때까지 기다리지 않고 변경 트랜잭션이 잠금을 획득하기 전에 있던 Row 값을 읽어 간다.

시간 Row의 현재 값 액션
00:01 X = 5 Transaction A : Row 잠금 획득
00:02   Transaction A : X = 6 변경
00:03   Transaction B : Row 읽기 X = 5
00:04 X = 6 Transaction A : X = 6 쓰기
00:05   Transactino A Row 잠금 해제

위와 같이 Transaction A가 변경을 위해 잠금을 획득하고 업데이트 중(00:01~00:05)이라고 하더라도 Transaction B는 00:03 시간에 X = 5인 변경 전의 값을 읽을 수 있다.

 

문제점 3 - Nonrepeatable Read

Read Committed가 보장되는 환경에서 사용자가 계좌 B(잔고 = 500원)에서 계좌 A(잔고 = 500원)로 돈(100원)을 전달하는 예제를 살펴보자.

시간 액션 결과
00:01 계좌 B에서 계좌 A로 100원 전달 요청
(계좌 A +100)
(계좌 B -100)
 
00:02 계좌 A 조회 A 계좌 잔고 = 500원
00:03 전달 완료  
00:04 계좌 B 조회 B  계좌 잔고 = 400원

Read Committed가 보장되기 때문에 계좌 B에서 계좌 A로 100원을 전달(00:01~00:03)하는 트랜잭션은 원자성과 격리성이 보장된다.

하지만 전달이 완료(트랜잭션 완료, 00:03)되기 전에 A 계좌를 조회(00:02)했다면 A 계좌는 전달되기 전의 잔고인 500원을 반환한다.

반면에 전달이 완료된 후 B 계좌를 조회(00:04)했다면 B 계좌는 전달된 후의 잔고인 400원을 반환한다.

이런 상황이 발생한다면 사용자는 1000원이 있지만 동시성 버그에 의해 900원이 있다고 조회될 것이다.

이러한 문제를 nonrepeatable read(비반복 읽기)라고 한다.

 

해결 방법 2 - Snapshot Isolation(Repeatable Read)

Snapshot Isolation은 nonrepeatable read를 해결할 수 있는 해결책이 된다.

각 트랜잭션은 데이터베이스의 일관된 Snapshot으로부터 데이터를 읽는다.

즉, 생성된 트랜잭션은 생성 당시의 데이터베이스에 commit되어 있는 현재의 모든 데이터를 본다.

따라서 트랜잭션 이후에 생성된 미래 데이터에 대해서는 볼 수 없고, 오직 과거에 커밋된 데이터만을 바라본다.

 

위의 nonrepeatable read의 문제였던 상황을 다시 확인해보자.

시간 트랜잭션 ID 액션 결과
00:01 1 계좌 B에서 계좌 A로 100원 전달 요청
(계좌 A +100)
(계좌 B -100)
 
00:02   트랜잭션 2 생성  
00:03 2 계좌 A 조회 A 계좌 잔고 = 500원
00:04   전달 완료  
00:05 2 계좌 B 조회 B  계좌 잔고 = 500원
00:06   트랜잭션 3 생성  
00:07 3 계좌 A 조회 A 계좌 잔고 = 600원
00:08 3 계좌 B 조회 B 계좌 잔고 = 400원

트랜잭션 2가 생성된 순간(00:02)은 아직 전달이 완료(commit)되지 않은 상태이기 때문에

각 계좌의 잔고를 조회(00:03, 00:05)했을 때 전달 전의 상태인 500원씩을 가지고 있다.

반면에 전달이 완료된 후 만들어진 트랜잭션 3은 전달이 완료된 금액인 600원/400원(00:07, 00:08)을 가진 것을 볼 수 있다.

이 같이 데이터베이스가 객체의 여러 버전을 함께 유지하는 기법을 다중 버전 동시성 제어(multi version concurrency control, MVCC)라고 한다. 

 

문제점 4 - Lost Update

Lost Update(갱신 손실) 문제는 주로 read-modify-write 주기를 가졌을 때 발생한다.

# read
SELECT value from counters WHERE key = 'foo';

# modify
int new_value = value+1;

# write
UPDATE counters SET value = new_value WHERE key = 'foo';

 

트랜잭션이 row의 특정 column을 증가시킬 때 기존 값을 읽고(read), 증가시키고(modify), 증가된 값을 쓴다(write).

이 때, 2개의 트랜잭션이 동시에 read-modify-write를 한다면 같은 값에 대해 수행하기 때문에 실제로 2만큼이 증가되지 않고 1만 증가된다.

 

이러한 문제는 대부분의 관계형 데이터베이스에서 원자적 쓰기 연산을 제공하는데 이를 통해 해결할 수 있다.

UPDATE counters SET value = value+1 WHERE key = 'foo';

 

또한 명시적 잠금을 이용해 값을 업데이트하는 트랜잭션이 해당 row를 잠금해 다른 트랜잭션이 읽지 못하도록 만들 수 있다.

BEGIN TRANSACTION;

SELECT * FROM object WHERE key = 'foo' FOR UPDATE; # 명시적 잠금

UPDATE object SET value = 'update' WHERE key = 'foo';

COMMIT;

 

문제점 5 - Write Skew & Phantom Read

Dirty Write와 Lost Update는 서로 다른 트랜잭션이 같은 객체에 동시에 쓰려고 할 때 발생한 문제점들이다.

Write Skew는 쓰기에서 문제가 발생한다는 점은 같지만 서로 다른 객체에 쓸 경우 발생하는 문제점이다.

다른 트랜잭션이 다른 객체에 쓰는게 어떤 문제가 되는걸까? 아래의 예를 살펴보자.

 

Tom은 쇼핑몰에서 포인트를 이용해서 물건을 사려고 한다.

쇼핑몰은 사용자의 포인트가 물품의 가격보다 크면 물품을 구매하는 order table에 이를 업데이트한다.

BEGIN TRANSACTION;

SELECT price FROM products WHERE product_id = 'foo';
SELECT point FROM points WHERE user_id = 'tom';

if (point >= price) {
    INSERT INTO order (user_id, product_id) VALUES ('tom', 'foo');
    UPDATE points SET point=point-price WHERE user_id = 'tom';
}

COMMIT;

 

만약 Tom이 2개의 트랜잭션을 이용해 'foo' product와 'bar' product을 동시에 포인트로 구매하려고 하면 어떻게 될까?

Snapshot Isoloation(Repeatable Read)이라면 'foo'를 구매하는 트랜잭션이 진행 중이더라도

동시에 생성된 트랜잭션의 snapshot에서 point는 차감이 안되었기 때문에 'bar' 물품 역시 구매가 되고 point는 음의 값을 가지게 될 수 있다.

Read Committed도 역시 물품 구매 중 값을 읽을 수 있기 때문에 Snapshot Isolation과 같은 문제점이 발생한다.

 

동시에 같은 객체에 쓰기가 아니기 때문에 원자성이나 격리성에 위배되지 않지만 분명한 문제가 발생한다.

이러한 문제를 해결하는 가장 좋은 방법은 한번에 하나의 트랜잭션만 실행하는 것이다.

뒤에서 나올 Serializability 수준을 사용하면 가능하지만 처리 속도의 이유로 이를 사용할 수 없다면 앞에 Lost Update의 해결 방법 중 하나였던 명시적 잠금을 이용해 해결할 수 있다.

BEGIN TRANSACTION;

SELECT price FROM products WHERE product_id = 'foo';
SELECT point FROM points WHERE user_id = 'tom' FOR UPDATE;

if (point >= price) {
    INSERT INTO order (user_id, product_id) VALUES ('tom', 'foo');
    UPDATE points SET point=point-price WHERE user_id = 'tom';
}

COMMIT;

 FOR UPDATE 를 사용해 points 테이블에서 user_id = 'tom'에 해당하는 row를 잠금하게 되면 해당 트랜잭션이 종료되기 전까지 다른 트랜잭션에서는 접근할 수 없게된다.

 

위와 같은 SELECT -> 조건 확인 -> UPDATE/INSERT와 같은 흐름에서 SELECT 부분에서 문제가 발생하는 경우를 Phantom Read라고 한다.

 

문제점 6 - Materializing Conflict (충돌 구체화)

위 Write Skew & Phantom Read는 명시적 잠금을 이용해 문제를 해결했다.

해결이 가능한 이유는 tom의 포인트 row와 같이 명시적 잠금을 할 대상이 정해져있기 때문이다.

하지만 조건 확인을 위한 SELECT에서 잠금할 대상이 없다면 Materializing Conflict(충돌 구체화) 문제가 발생한다.

예시를 위해 회의실 예약 시스템을 생각해보자.

BEGIN TRANSACTION;

SELECT count(*) FROM bookings 
WHERE start_time < '2020-07-31 14:00' 
AND end_time > '2020-07-31 15:00';

if (count == 0) {
    INSERT INTO bookings (start_time, end_time) VALUES ('2020-07-31 14:00', '2020-07-31 15:00');
}

COMMIT;

14:00에서 15:00 사이에 회의가 없다면 회의실을 예약한다.

조건 확인을 위해 조회한 값은 예약 시간에 존재하는 예약 정보인데 이를 잠금하는건 충돌 문제를 해결하는데 도움이 되지 않는다.

회의실 예약을 위해서는 조회된 값이 없어야 하고 이 때, 예약을 위해 Write를 진행한다.

하지만 Write 중에 다른 트랜잭션에서 동일한 내용이 실행됐다면 그 어떤 잠금도 없기 때문에 충돌이 발생한다.

 

이러한 경우에는 Serializability(직렬성) 수준의 트랜잭션을 이용해 한 순간에 하나의 트랜잭션만 실행되도록 해야한다.

 

해결 방법 3 - Serializability(직렬성)

동시성 문제를 해결하는 가장 좋은 방법은 동시성을 없애는 것이다.

Serializability는 동시성을 없애고 병렬적으로 트랜잭션이 들어와도 한 순간에 오직 하나의 트랜잭션만 처리한다.

이렇게 되면 앞에서 발생했던 모든 문제를 해결 할 수 있다.

 

직렬성을 이용할 때는 트랜잭션에서 실행해야 하는 구문을 모두 Database에 미리 생성해 놓는 stored procedure를 주로 사용한다.

 

직렬성의 장점 :

- 동시성 문제(Dirty Write, Lost Update, Wirte Skew, Materializing Conflict)가 발생하지 않는다.

 

직렬성의 단점 :

- 하나의 thread만 이용하기 때문에 Database 단일 코어의 성능이 중요해진다.

- 하나의 트랜잭션만 처리하기 때문에 시간 당 처리량이 낮아진다.

 

정리

Database에서 지원하는 트랜잭션의 수준(Level)에 대해 알아봤다.

몇몇 NoSQL은 이러한 트랜잭션을 포기하고 높은 처리 속도와 분산 저장을 선택한 경우도 많다.

어떤 트랜잭션 수준을 사용하든 또는 아예 사용하지 않든 중요한 것은 애플리케이션에서 필요로 하는 정도의 수준에서

Best Practice를 파악하고 적용하는 것 같다.

 

TPC - The Transaction Processing Performance Council

 

TPC는 OLTP database의 성능 측정(benchmark)를 위해 만들어졌고, TPC-C는 이 중 복잡한 트랜잭션을 처리하는 상황을 시뮬레이션 할 수 있는 database의 table 구조

 

TPC-C table 구조

https://programmer.help/blogs/tpcc-instructions.html

 

테이블의 이름에서 확인 할 수 있듯이 일반적인 e-commerce 서비스 table 구조와 유사한 형태를 가지고 있다.

 

위 테이블에 많은 데이터를 집어 넣으면서 성능을 측정 할 수 있는데 생성할 트랜잭션의 갯수 역시 정해져있다.

 

Warehouse(W)에 따른 생성되는 데이터의 갯수

https://programmer.help/blogs/tpcc-instructions.html

창고(Warehouse) 하나가 생성되면 약 30,000개의 주문(Order)가 생기는 식으로 계산할 수 있다.

 

tpcc-mysql 을 이용해 MySQL에 Warehouse가 한 개인 데이터를 생성해보자.

mac을 이용해서 tpcc-mysql를 빌드했을 때 에러가 발생하여 docker-compose를 이용해 테스트를 진행했다.

 

tpcc-mysql docker image 만들기

$ git clone https://github.com/Percona-Lab/tpcc-mysql.git
$ cd tpcc-mysql
$ docker build . -t tpcc-mysql
$ docker images
...
tpcc-mysql              latest
...

 

docker-compose를 이용해 mysql과 tpcc-mysql 실행하기

 

docker-compose.yml

version: "3"
services:
    mysql:
        image: mysql:5.7
        environment:
          MYSQL_DATABASE: tpcc
          MYSQL_ROOT_PASSWORD: root
          MYSQL_ROOT_HOST: '%'
        ports:
          - 3306:3306
        volumes:
          - mysql_volume:/var/lib/mysql
    tpcc:
      image: tpcc-mysql:latest
      command: tail -f /dev/null
      links:
      - mysql

volumes:
    mysql_volume:

mysql container가 재시작 된 이후에도 데이터를 보존하기 위해 docker volume을 사용했다.

$ docker-compose up -d
Creating network "tpcc-mysql_default" with the default driver
Creating tpcc-mysql_mysql_1 ... done
Creating tpcc-mysql_tpcc_1  ... done
$ docker ps
IMAGE               COMMAND                  CREATED             STATUS              PORTS                               NAMES
eb2faaa6072f        tpcc-mysql:latest   "tail -f /dev/null"      13 seconds ago      Up 12 seconds                                           tpcc-mysql_tpcc_1
9ecafd85f8bb        mysql:5.7           "docker-entrypoint.s…"   14 seconds ago      Up 12 seconds       0.0.0.0:3306->3306/tcp, 33060/tcp   tpcc-mysql_mysql_1

docker-compose를 통해 mysql과 tpcc-mysql 을 실행했다면 tpcc-mysql으로 들어가 benchmark 작업을 수행할 수 있다.

$ docker exec -it <tpcc-mysql Container ID> bash
$ root@eb2faaa6072f:/tpcc-mysql#

이제 Database와 Table을 만들어 보자.

# tpcc1 database 생성
$ mysqladmin create tpcc1 -hmysql -uroot -proot
mysqladmin: [Warning] Using a password on the command line interface can be insecure.

# tpcc1에 table 생성
$ mysql tpcc1 -hmysql -uroot -proot < create_table.sql
mysql: [Warning] Using a password on the command line interface can be insecure.

docker exec 를 통해 tpcc-mysql cotainer에 접속했듯이 mysql container에 접속하면 생성된 table을 확인 할 수 있다.

mysql> use tpcc1;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed

mysql> show tables;
+-----------------+
| Tables_in_tpcc1 |
+-----------------+
| customer        |
| district        |
| history         |
| item            |
| new_orders      |
| order_line      |
| orders          |
| stock           |
| warehouse       |
+-----------------+

table의 foreign key와 index에 관한 정보를 추가하고 싶다면 아래 명령어를 추가로 실행하면 된다.

아래 명령어는 데이터가 생성된 이후에 진행해도 무관하다.

$ mysql tpcc1 -hmysql -uroot -proot < add_fkey_idx.sql

데이터를 생성하기 위한 table이 설정이 완료되었고, benchmark를 위한 초기 데이터를 생성해보자.

 

warehouse의 개수는 1개(-w1)로 설정했다.

$ ./tpcc_load -hmysql -dtpcc1 -uroot -proot -w1
*************************************
*** TPCC-mysql Data Loader        ***
*************************************
option h with value 'mysql'
option d with value 'tpcc1'
option u with value 'root'
option p with value 'root'
option w with value '1'
<Parameters>
     [server]: mysql
     [port]: 3306
     [DBname]: tpcc1
       [user]: root
       [pass]: root
  [warehouse]: 1
TPCC Data Load Started...
Loading Item
.................................................. 5000
...
...DATA LOADING COMPLETED SUCCESSFULLY.

제대로 데이터가 생성되었는지 확인하기 위해 mysql을 통해 확인할 수 있다.

mysql> select count(*) from warehouse;
+----------+
| count(*) |
+----------+
|        1 |
+----------+
1 row in set (0.00 sec)

mysql> select count(*) from orders;
+----------+
| count(*) |
+----------+
|    30000 |
+----------+
1 row in set (0.01 sec)

warehouse를 1개 생성했기 때문에 30K*W 개수만큼 생성되는 orders는 30,000개가 생성된걸 확인할 수 있다.

 

이제 생성된 데이터를 기반으로 benchmark 실행해보자.

$ ./tpcc_start -hmysql -P3306 -dtpcc1 -uroot -proot -w1 -c32 -r10 -l100
***************************************
*** ###easy### TPC-C Load Generator ***
***************************************
option h with value 'mysql'
option P with value '3306'
option d with value 'tpcc1'
option u with value 'root'
option p with value 'root'
option w with value '1'
option c with value '32'
option r with value '10'
option l with value '100'
<Parameters>
     [server]: mysql
     [port]: 3306
     [DBname]: tpcc1
       [user]: root
       [pass]: root
  [warehouse]: 1
 [connection]: 32
     [rampup]: 10 (sec.)
    [measure]: 100 (sec.)

RAMP-UP TIME.(10 sec.)

MEASURING START.

  10, trx: 1466, 95%: 45.707, 99%: 66.588, max_rt: 134.603, 1463|259.705, 147|48.093, 147|117.481, 146|234.267
  20, trx: 1491, 95%: 44.921, 99%: 60.163, max_rt: 80.195, 1491|258.610, 149|25.793, 149|110.530, 151|144.989
  30, trx: 1476, 95%: 45.394, 99%: 62.963, max_rt: 91.697, 1473|257.982, 147|29.405, 148|101.045, 147|134.593
  40, trx: 1487, 95%: 42.424, 99%: 57.919, max_rt: 76.369, 1486|251.475, 149|38.944, 148|102.436, 149|165.392
  50, trx: 1523, 95%: 42.488, 99%: 57.797, max_rt: 74.825, 1533|246.987, 152|38.309, 153|110.212, 151|174.092
  60, trx: 1508, 95%: 43.661, 99%: 60.760, max_rt: 114.722, 1498|244.906, 151|52.633, 151|147.776, 152|150.247
  70, trx: 1479, 95%: 45.996, 99%: 57.521, max_rt: 81.752, 1483|268.676, 148|45.097, 147|118.265, 147|168.063

-w : benchmark에 사용할 warehouse의 개수

-c : benchmark에 사용할 database connection의 개수

-r : bechmark 테스트 주기 (Ramp-Up Time)

-l : benchmark의 Runnig Time

 

측정 결과를 간단하게 해석해보면 아래와 같다.

처음 10초 동안 1466개의 New Order트랜잭션이 발생

New Order 트랜잭션 중 95%에 해당하는 트랜잭션의 소요시간이 45.707 초, 99%가 66.588초가 걸림

가장 오래 걸린 New Order 트랜잭션 시간은 134.603 초가 걸림

New Order 외의 트랜잭션에 대해서 throughput과 max response 시간은 각각

1463|259.705, 147|48.093, 147|117.481, 146|234.267 이 됨

 

TPC benchmark는 OLTP 데이터베이스의 성능을 측정하는게 주된 목표이지만

앞으로 MySQL에 대해 공부하면서 주로 사용할 것 같아 정리해봤다.

 

mysql 공식페이지에서는 이러한 benchmark를 사용자가 직접 커스텀할 수 있는 툴인 mysqlslap을 제공하고 있다.

https://dev.mysql.com/doc/refman/5.7/en/custom-benchmarks.html

 

 

 

 

본 포스팅은 Udemy Certificated Kubernetes Administrator 강좌를 공부한 내용입니다.

 

Certified Kubernetes Administrator (CKA) Practice Exam Tests

Prepare for the Certified Kubernetes Administrators Certification with live practice tests right in your browser - CKA

www.udemy.com

Kubernetes Upgrade

Kubernetes 역시 소프트웨어이기 때문에 버전이 존재하고 하위 버전을 호환하기 위한 backward compatibility를 갖추고 있어야 한다.

Kubernetes는 Master Node에서 동작하는 ControlPlane의 구성요소와

Worker Node에서 동작하는 kubelet, kube-proxy,

사용자가 사용하는 kubectl 등 다양한 프로세스로 구성되어 있다.

 

그리고 다양한 프로세스는 같은 버전을 유지하는 것을 권장한다.

하지만 모든 프로세스의 버전을 맞출 수 없다면, Kubernetes는 프로세스의 모든 버전에 대해 backward compatibility를 갖추고 있어야 할까?

현실적으로 그것은 불가능하기 때문에 Kubernetes는 버전과 프로세스의 하위 버전 지원의 범위를 정해놓고 있다.

Kubernetes의 버전은 api-server의 버전을 기준으로 한다.

 

프로세스 지원 버전 범위 예시
kube-apiserver X v1.10
controller-manager X-1 v1.9 or v1.10
kube-scheduler X-1 v1.9 or v1.10
kubelet X-2 v1.8 or v1.9 or v1.10
kube-proxy X-2 v1.8 or v1.9 or v1.10
kubectl X+1 > X-1 v1.9 or v1.10 or v1.11

 

또한 kubernetes를 업그레이드 할 때에는 minor 버전을 하나씩 올리는 것을 권장한다. (eg,. v1.9 -> v1.10 -> v1.11)

(버전 명 : v{major}.{minor}.{patch})

 

Kubernetes를 설치할 때와 마찬가지로 업그레이드 시에도 Manual하게 하는 방법과 kubeadm을 사용하는 방법이 있다.

이번에도 역시 kubeadm으로 업그레이드를 진행한다.

 

 

Upgrade Strategy

Kubernetes는 여러 노드로 구성되어 있기 때문에 모든 노드를 전부 업그레이드 해주어야 한다.

이 과정에서 노드의 downtime에 따른 3가지 전략이 있다.

 

1. Master 노드를 업그레이드한 후, 모든 Worker 노드를 한 번에 업그레이드 한다.

당연하겠지만 모든 Worker 노드의 kubelet, kube-proxy 등이 한번에 재시작 되기 때문에 클러스터에서 동작하는 Pod의 downtime이 존재한다.

 

2. Master 노드를 업그레이드한 후, 한 Worker 노드씩 업그레이드 한다.

Worker 노드를 하나씩 재시작하며 업그레이드를 진행한다. 업그레이드 하기 전 node drain을 통해 노드에 있는 Pod를 퇴출시킨 뒤 업그레이드 한다.

 

3. 업그레이드가 이미 된 새로운 Worker 노드를 추가하고 기존 노드 하나를 업그레이드 한다.

언뜻 2번과 비슷한 전략 같지만 이렇게 진행하기 위해선 전체 Worker 노드 수 + 1개의 노드가 필요하다. 업그레이드 중에도 노드의 갯수를 동일하게 유지할 수 있기 때문에 클러스터의 리소스 양을 유지할 수 있다.

 

Kubeadm Upgrade

이제 kubeadm을 사용해 노드 업그레이드를 진행해보자.

$ kubectl get node
NAME     STATUS   ROLES    AGE    VERSION
master   Ready    master   112s   v1.16.0
node01   Ready    <none>   82s    v1.16.0

2개의 노드가 있고 각 노드의 버전은 v1.16.0이다.

노드 하나씩 업그레이드 하는 Strategy 2를 사용해서 업그레이드 해보자.

kubeadm을 사용하면 현재 업그레이드 할 수 있는 버전을 알 수 있다.

$ kubeadm upgrade plan
[upgrade/config] Making sure the configuration is correct:
[upgrade/config] Reading configuration from the cluster...
...
...
Components that must be upgraded manually after you have upgraded the control plane with 'kubeadm upgrade apply':
COMPONENT   CURRENT       AVAILABLE
Kubelet     2 x v1.16.0   v1.17.7

Upgrade to the latest stable version:

COMPONENT            CURRENT   AVAILABLE
API Server           v1.16.0   v1.17.7
Controller Manager   v1.16.0   v1.17.7
Scheduler            v1.16.0   v1.17.7
Kube Proxy           v1.16.0   v1.17.7
CoreDNS              1.6.2     1.6.5
Etcd                 3.3.15    3.4.3-0

You can now apply the upgrade by executing the following command:

        kubeadm upgrade apply v1.17.7

Note: Before you can perform this upgrade, you have to update kubeadm to v1.17.7.

v1.17.7 버전으로 업그레이드 할 수 있다고 나온다.

 

v1.17.7으로 업그레이드 하기 위해서는 kubeadm의 버전이 일치해야한다.

일치하지 않을 경우 kubeadm 버전을 업그레이드 하고

kubeadm upgrade plan v1.17.7을 통해 해당 버전으로 업그레이드가 가능한지 확인한다.

$ apt-get upgrade kubeadm=1.17.7-00

$ kubeadm version
kubeadm version: &version.Info{Major:"1", Minor:"17", GitVersion:"v1.17.7", GitCommit:"b4455102ef392bf7d594ef96b97a4caa79d729d9", GitTreeState:"clean", BuildDate:"2020-06-17T11:37:34Z", GoVersion:"go1.13.9", Compiler:"gc", Platform:"linux/amd64"}

$ kubeadm upgrade plan v1.17.7
[upgrade/config] Making sure the configuration is correct:
[upgrade/config] Reading configuration from the cluster...
[upgrade/config] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -oyaml'
[preflight] Running pre-flight checks.
[upgrade] Making sure the cluster is healthy:
[upgrade] Fetching available versions to upgrade to
[upgrade/versions] Cluster version: v1.16.0
[upgrade/versions] kubeadm version: v1.17.7

Components that must be upgraded manually after you have upgraded the control plane with 'kubeadm upgrade apply':
COMPONENT   CURRENT       AVAILABLE
Kubelet     2 x v1.16.0   v1.17.7

Upgrade to the latest version in the v1.16 series:

COMPONENT            CURRENT   AVAILABLE
API Server           v1.16.0   v1.17.7
Controller Manager   v1.16.0   v1.17.7
Scheduler            v1.16.0   v1.17.7
Kube Proxy           v1.16.0   v1.17.7
CoreDNS              1.6.2     1.6.5
Etcd                 3.3.15    3.4.3-0

You can now apply the upgrade by executing the following command:

        kubeadm upgrade apply v1.17.7

_____________________________________________________________________

 

 

업그레이드를 하기 전 노드에서 Pod를 퇴출시키는 명령어에 대해 알아보자.

# <NODE>의 Pod 퇴출 및 unscheduling
$ kubectl drain <NODE>

# <NODE> unscheduling
$ kubectl cordon <NODE>

# <NODE> scheduling
$ kubectl uncordon <NODE>

drain은 현재 Pod를 모두 퇴출시키고 앞으로도 kube-scheduler에 의해 Pod가 해당 노드로 스케쥴링 되지 않는다.

$ kubectl describe node master
Name:               master
...
...
Taints:             node.kubernetes.io/unschedulable:NoSchedule

$ kubectl get node
NAME     STATUS                     ROLES    AGE   VERSION
master   Ready,SchedulingDisabled   master   11m   v1.16.0
node01   Ready                      <none>   10m   v1.16.0

위와 같이 스케쥴링이 되지 않도록 Taint가 설정되고 STATUS에 SchedulingDisabled가 나온다.

cordon은 drain처럼 스케쥴링이 되지 않지만 현재 실행중인 Pod는 퇴출시키지 않는다.

uncordon은 drain/cordon된 node를 다시 스케쥴링 할 수 있도록 변경한다.

 

 

master 노드에 있는 Pod를 퇴출시킨 두 업그레이드 한다.

$ kubectl drain master --ignore-daemonsets
node/master already cordoned
WARNING: ignoring DaemonSet-managed Pods: kube-system/kube-proxy-7qcdn, kube-system/weave-net-k2z8q
node/master drained

$ kubeadm upgrade apply v1.17.7
...
...
[upgrade/successful] SUCCESS! Your cluster was upgraded to "v1.17.7". Enjoy!
...

$ kubectl get node
NAME     STATUS                     ROLES    AGE   VERSION
master   Ready,SchedulingDisabled   master   31m   v1.16.0
node01   Ready                      <none>   30m   v1.16.0

$ apt-get install kubelet=1.17.7-00

$ kubectl get node
NAME     STATUS                     ROLES    AGE   VERSION
master   Ready,SchedulingDisabled   master   33m   v1.17.7
node01   Ready                      <none>   32m   v1.16.0

kubeadm으로 업그레이드를 한 뒤 kubelet까지 업그레이드를 해야 노드의 업그레이드가 완료된다.

 

kubeadm과 kubelet은 자동적으로 업그레이드가 되면 안되기 때문에 자동 업그레이드를 막아준다.

$ apt-mark hold kubeadm
kubeadm set on hold.

$ apt-mark hold kubelet
kubelet set on hold.

 

이제 master노드를 다시 스케쥴링 될 수 있도록 만든다.

$ kubectl uncordon master
node/master uncordoned

$ kubectl get node
NAME     STATUS   ROLES    AGE   VERSION
master   Ready    master   34m   v1.17.7
node01   Ready    <none>   33m   v1.16.0

 

이제 node01을 업그레이드 해보자.

$ kubectl drain node01
WARNING: ignoring DaemonSet-managed Pods: kube-system/kube-proxy-hggqf, kube-system/weave-net-rw9th
evicting pod kube-system/coredns-6955765f44-pwtbn
evicting pod kube-system/coredns-6955765f44-tnbjn
pod/coredns-6955765f44-pwtbn evicted
pod/coredns-6955765f44-tnbjn evicted
node/node01 evicted

$ ssh node01

node01 $ kubeadm upgrade node
[upgrade] Reading configuration from the cluster...
[upgrade] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -oyaml'
[upgrade] Skipping phase. Not a control plane node[kubelet-start] Downloading configuration for the kubelet from the "kubelet-config-1.17" ConfigMap in the kube-system namespace
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[upgrade] The configuration for this node was successfully updated!
[upgrade] Now you should go ahead and upgrade the kubelet package using your package manager.

node01 $ apt-get install kubelet=1.17.7-00

node01 $ apt-mark hold kubeadm
kubeadm set on hold.

node01 $ apt-mark hold kubelet
kubelet set on hold.

node01 $ logout

$ kubectl uncordon node01
node/node01 uncordoned

$ kubectl get node
NAME     STATUS   ROLES    AGE   VERSION
master   Ready    master   42m   v1.17.7
node01   Ready    <none>   42m   v1.17.7

 

이미 master 노드에서 업그레이드한 kubernetes의 정보가 있기 때문에 kubeadm upgrade node 를 통해서 업그레이드 할 수 있다.

 

정리하자면, 업그레이드 순서는 아래와 같다.

 

1. master node drain (kubectl drain <MASTER_NODE>)

2. upgrade master node (apt-get install kubeadm=<VERSION> kubeadm upgrade apply <VERSION>

3. upgrade kubelet (apt-get install kubelet=<VERSION>)

4. master node uncordon (kubectl uncordon <MASTER_NODE>)

 

5. worker node drain (kubectl drain <WORKER_NODE>)

6. upgrade worker node (kubeadm upgrade node)

7. upgrade kubelet (apt-get install kubelet=<VERSION>)

8. worker node uncordon (kubectl uncordon <WORKER_NODE>)

+ Recent posts