들어가며

 카프카는 기본적으로 1MB 이상의 큰 메시지를 고려하고 만들어진 메시지 시스템이 아닙니다. 하지만 그럼에도 1MB 이상의 메시지로 파이프라인을 구성해야 할 때가 있습니다. 예로 이미지 혹은 정말 큰 JSON 메시지로 파이프라인 구성할 때가 있습니다. 이때, 프로듀서, 브로커, 컨슈머는 그 파이프라인 성격에 맞게 직접적 혹은 간접적으로 영향을 받는 설정들을 조정해줘야 합니다. 이번 글은 각 카프카 컴포넌트들이 1MB 이상의 메시지를 보낼 때 고려해야 하는 설정들을 정리했습니다.

 참고로 아래 설정들을 전부 테스트해보지 못했습니다. 몇 가지 설정은 주관적인 판단에 의해 명시되어 있습니다. 혹여나 잘못되거나 부족한 부분은 댓글로 피드백 부탁드립니다. :)

1. 프로듀서 측

1.1. 직접적인 영향을 미치는 설정들

max.request.size

  • 프로듀서가 브로커에 한 번 요청할 때 보낼 수 있는 최대 크기
  • 메시지 크기가 이 설정 값보다 클 경우 프로듀싱 불가
  • 브로커의 message.max.byte 설정과 연관
  • 기본 값 : 1048576 (1MB)

1.2. 간접적인 영향을 미치는 설정들

buffer.memory

  • 프로듀서가 버퍼(+압축)에 사용할 총메모리의 양
  • 메시지 자체가 커지므로 버퍼의 양도 커져야 함
  • 기본 값 : 33554432 (32MB)

request.timeout.ms

  • 프로듀서가 요청 후 브로커의 응답을 대기하는 최대 시간
  • 만약 ack=all 인 상태에서 메시지 크기가 클 경우, 브로커 내부에서 복제할 때 시간이 걸릴 수 있음
  • 기본 값 : 30000 (30초)

2. 브로커 측

2.1. 직접적인 영향을 미치는 설정들

message.max.bytes

  • 레코드 배치(단일 요청)의 최대 크기
    • 토픽 별로 설정 가능 (max.message.bytes)
  • 이 값보다 클 경우 메시지를 받을 수 없음
  • 프로듀서의 max.request.size 설정과 연관
  • 기본 값 : 1048588 (1MB)

fetch.max.bytes

  • 각 fetch 요청에 따라 반환할 최대 바이트 수
  • 이 설정보다 메시지 바이트가 클 경우 처리되지 않음
  • 기본 값 : 57671680 (55MB)

2.2. 간접적인 영향을 미치는 설정들

log.segment.bytes

  • 각 세그먼트 파일의 크기
  • 메시지의 크기가 커질수록 기존보다 더 많은 세그먼트가 만들어지고, 이는 파일 핸들러 자원을 더 많이 점유하게 됨
  • 기본 값 : 1073741824 (1GB)

request.timeout.ms

  • 요청하고 응답까지 대기하는 최대 시간
  • 메시지 크기가 커질수록 대기 시간이 길어질 확률이 높아짐
  • 기본 값 : 30000 (30초)

replica.fetch.max.bytes

  • 각 복제 과정에서 fetch 시도할 수 있는 최대 바이트 수
  • 만약 이 설정보다 메시지 크기가 크다면 별도의 조정 과정이 진행
  • 기본 값 : 1048576 (1MB)

replica.fetch.response.max.bytes

  • 전체 fetch 응답의 최대 바이트 수
  • 만약 이 설정보다 메시지 크기가 크다면 별도의 조정 과정이 진행
  • 기본 값 : 10485760 (10MB)

3. 컨슈머 측

3.1. 직접적인 영향을 미치는 설정들

없는 것으로 판단됩니다.

3.2. 간접적인 영향을 미치는 설정들

max.partition.fetch.bytes

  • 브로커에서 각 파티션 별로 최대로 반환할 수 있는 바이트 수
  • 만약 이 설정보다 메시지 크기가 크다면 별도의 조정 과정이 진행
  • 기본 값 : 1048576 (1MB)

fetch.max.bytes

  • 브로커에서 최대로 반환할 수 있는 바이트 수
  • 만약 이 설정보다 메시지 크기가 크다면 별도의 조정 과정이 진행
  • 기본 값 : 52428800 (50MB)

max.poll.interval.ms

  • poll() 메서드 처리할 때, 컨슈머 그룹에서의 최대 대기 시간
  • 메시지 크기가 커지고 처리에 시간이 길어진다면 컨슈머 그룹에서 제외되고 컨슈머 그룹 리밸런싱이 발생할 확률이 높아짐
  • 기본 값 : 300000 (5분)

request.timeout.ms

  • 요청하고 응답까지 대기하는 최대 시간
  • 메시지 크기가 커질수록 대기 시간이 길어질 확률이 높아짐
  • 기본 값 : 30000 (30초)

출처: https://always-kimkim.tistory.com/entry/kafka-operations-settings-concerned-when-the-message-has-more-than-1-mb [언제나 김김:티스토리]

MySQL 서버를 포함한 RDBMS를 사용하다 보면, No-SQL DBMS 서버에 비해서 많은 데이터 타입들을 가지고 있다는 것을 알고 있을거에요. 하지만 RDBMS를 사용하면서 이런 다양한 데이터 타입에 대해서 정확한 용도와 특성을 모르면 RDBMS 서버가 어렵게 구현하고 있는 장점을 놓쳐 버릴 가능성이 높아요.

 

오늘은 많은 개발자와 DBA들이 잘 모르고 있는 MySQL 서버의 VARCHAR  TEXT 타입의 특성과 작동 방식에 대해서 좀 살펴보려고 해요.

 

VARCHAR 타입 궁금증

MySQL 서버를 사용해 본 개발자라면 누구나 한번쯤은 이런 궁금증을 가져 본 적이 있을거에요.

  • 만약 10 글자 이하로만 저장된다면 컬럼의 타입을 VARCHAR(10)으로 하거나 VARCHAR(1000)으로 해도 아무런 차이가 없는 것 아닐까? 오히려 VARCHAR(1000)으로 만들어 두면 나중에 더 큰 값을 저장해야 할 때 더 유연하게 대응할 수 있지 않을까 ?

아래와 같이 모든 컬럼을 VARCHAR(1000) 타입으로 또는 모든 컬럼을 TEXT 타입으로 생성한 테이블은 어떻게 생각하나요 ? 이런 모델링이 잘못되었다고 생각한다면 그 근거는 무엇인가요 ?

CREATE TABLE user (
  id       BIGINT NOT NULL,
  name     VARCHAR(1000),
  phone_no VARCHAR(1000),
  address  VARCHAR(1000),
  email    VARCHAR(1000),
  PRIMARY KEY(id)
);

그냥 지금까지 습관적으로 해오던 데이터 모델링 방법과는 다르기 때문에 잘못된 것일까요 ? MySQL 서버가 내부적으로 어떻게 작동하는지를 모르면, 이 질문에 명확한 답변을 하기는 어려울 수 있어요.

테이블의 컬럼이 많은 경우, 이 질문에 대해서 명확한 답변이 될만한 근거가 한가지 있어요. 간단한 테스트를 위해서 길이가 매우 긴 VARCHAR 컬럼을 가진 테이블을 한번 만들어 볼까요 ?


mysql> CREATE TABLE tb_long_varchar (id INT PRIMARY KEY, fd1 VARCHAR(1000000));
ERROR 1074 (42000): Column length too big for column 'fd1' (max = 16383); use BLOB or TEXT instead

mysql> CREATE TABLE tb_long_varchar (id INT PRIMARY KEY, fd1 VARCHAR(16383));
ERROR 1118 (42000): Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535. This includes storage overhead, check the manual. You have to change some columns to TEXT or BLOBs

mysql> CREATE TABLE tb_long_varchar (id INT PRIMARY KEY, fd VARCHAR(16382));
Query OK, 0 rows affected (0.19 sec)

mysql> ALTER TABLE tb_long_varchar ADD fd2 VARCHAR(10);
ERROR 1118 (42000): Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535. This includes storage overhead, check the manual. You have to change some columns to TEXT or BLOBs

tb_long_varchar 테이블은 하나의 VARCHAR 컬럼이 있는데, VARCHAR 타입의 최대 저장 가능 길이를 어느 정도로 하느냐에 따라서 테이블을 생성하지 못하게 되는 것을 확인할 수 있어요. 그리고 네번째 ALTER TABLE 문장의 실행 예제를 보면, 새로운 컬럼 추가가 실패한 것을 알 수 있어요. 이는 (에러 메시지에서도 잘 설명하고 있듯이) 이미 tb_long_varchar 테이블은 하나의 레코드가 저장할 수 있는 최대 길이가 65,535 바이트를 초과했기 때문에 더이상 새로운 컬럼을 추가할 수 없게 된 거에요.

이 예제를 통해서 MySQL 서버에서는 하나의 VARCHAR 컬럼이 너무 큰 길이를 사용하면, 다른 컬럼들이 사용할 수 있는 최대 공간의 크기가 영향을 받게 된다는 것을 확인했어요. 그래서 MySQL 서버에서는 레코드 사이즈 한계로 인해서, VARCHAR 타입의 최대 저장 길이 설정시에 공간을 아껴서 설정해야 해요.

이는 MySQL 서버 메뉴얼에서 이미 자세히 설명하고 있어요. 참고로 TEXT나 BLOB와 같은 LOB 컬럼은 이 제한 사항에 거의 영향을 미치지 않아요. 그래서 많은 컬럼을 가진 테이블에서는 VARCHAR 타입 대신 TEXT 타입을 사용해야 할 수도 있어요.

그런데 VARCHAR 타입의 길이 설정에 주의해야 하는 이유가 이거 하나 뿐일까요 ? 예를 들어서 추가로 새로운 컬럼이 필요치 않아서 아래와 같이 테이블을 모델링했다면, 이건 아무 문제가 없는 걸까요 ?

CREATE TABLE user (
  id       BIGINT NOT NULL,
  name     VARCHAR(4000),
  phone_no VARCHAR(4000),
  address  VARCHAR(4000),
  email    VARCHAR(4000),
  PRIMARY KEY(id)
);

TEXT 타입 궁금증

VARCHAR 타입은 저장 길이 설정에 대한 주의가 필요하다는 것을 간단히 한번 살펴보았어요. 그런데 VARCHAR 대신 TEXT 타입을 사용하면 길이 제한 문제가 싹 사라진다는 것을 쉽게 확인할 수 있어요. 그래서 아래와 같이 테이블을 만들면 VARCHAR 타입의 길이 설정에 대한 제약뿐만 아니라 저장하는 값의 길이 제한도 훨씬 크고 유연하게 테이블을 만들 수 있어요.

CREATE TABLE user (
  id       BIGINT NOT NULL,
  name     TEXT,
  phone_no TEXT,
  address  TEXT,
  email    TEXT,
  PRIMARY KEY(id)
);

여기에서 또 하나의 궁금증이 생기기 시작할거에요.

  • 문자열 저장용 컬럼을 생성할 때, VARCHAR와 TEXT 중에서 굳이 VARCHAR를 선택할 이유가 있을까 ? TEXT 타입은 저장 가능 길이도 훨씬 크고 테이블 생성할 때 굳이 길이 제한을 결정하지 않아도 되니 더 좋은 것 아닐까 ? 근데 왜 우리가 모델링하는 테이블에서 대부분 문자열 저장용 컬럼은TEXT 컬럼이 아니라 VARCHAR 컬럼이 사용될까 ?
 

VARCHAR vs TEXT

일반적인 RDBMS에서, TEXT(또는 CLOB)나 BLOB와 같은 대용량 데이터를 저장하는 컬럼 타입을 LOB(Large Object) 타입이라고 해요. 그리고 RDBMS 서는 LOB 데이터를 Off-Page 라고 하는 외부 공간에 저장해요. 일반적인 RDBMS에서와 같이, MySQL 서버도 레코드의 컬럼 데이터는 B-Tree (Clustering Index)에 저장(이를 Inline 저장이라고 함)하지만, 용량이 큰 LOB 데이터는 B-Tree 외부의 Off-Page 페이지(MySQL 서버 메뉴얼에서는 External off-page storage라고 해요)로 저장해요.

하지만 MySQL 서버는 LOB 타입의 컬럼을 항상 Off-Page로 저장하지는 않고, 길이가 길어서 저장 공간이 많이 필요한 경우에만 Off-Page로 저장해요.

예를 들어서 아래와 같이 2개의 레코드가 있을 때, 1번 레코드(id=1)의 fd 컬럼에 8,100 글자(8,100바이트)를 저장하면 Off-Page가 아닌 B-Tree(Clustering Index)에 Inline으로 저장해요. 하지만 2번 레코드(id=2)의 fd 컬럼에 8,101글자(8,101바이트)를 저장하면, MySQL 서버는 fd 컬럼을 Off-Page로 저장해요. 이는 MySQL 서버의 레코드 포맷에 따라서 조금씩 다르게 작동하는데, 이 예제는 innodb_default_row_format=DYNAMIC 설정을 기준으로 테스트해본 결과에요.

CREATE TABLE tb_lob (
  id INT PRIMARY KEY,
  fd TEXT
);

INSERT INTO tb_lob VALUES (1, REPEAT('A',8100));  -- // Inline 저장소
INSERT INTO tb_lob VALUES (2, REPEAT('A',8101));  -- // Off-Page 저장소

MySQL 서버의 레코드 크기 제한은 65,535 바이트이지만, InnoDB 스토리지 엔진의 레코드 크기 제한은 페이지(블록)의 크기에 따라서 달라지는데, 대부분 페이지 크기의 절반이 InnoDB 스토리지 엔진의 최대 레코드 크기 제한으로 작동해요.

InnoDB 스토리지 엔진은 레코드의 전체 크기가 이 제한 사항(16KB 페이지에서는 8,117 바이트)을 초과하면 길이가 긴 컬럼을 선택해서 Off-Page로 저장하게 되는데, 이 예제의 두번째 레코드(id=2)의 fd 컬럼 값이 커서 이 컬럼을 Off-Page로 저장한 것이에요.

MySQL 서버의 InnoDB row_format에 따른 Off-Page 저장 방식 차이는 MySQL 서버 메뉴얼을 참고해주세요. 페이지 크기가 64KB인 경우 InnoDB 최대 레코드 크기의 제한 사항이 예외적으로 조금 다르므로, MySQL 서버와 InnoDB 스토리지 엔진의 레코드 크기 제한에 대한 자세한 설명은 MySQL 서버 메뉴얼을 참고해주세요.

그런데 동일한 테스트를 아래와 같이 VARCHAR 타입 컬럼으로 해보면, VARCHAR 컬럼에 저장된 값이 큰 경우에도 Off-Page로 저장된다는 것이에요.

CREATE TABLE tb_varchar (
  id INT PRIMARY KEY,
  fd VARCHAR
);

INSERT INTO tb_varchar VALUES (1, REPEAT('A',8100)); -- // Inline 저장소
INSERT INTO tb_varchar VALUES (2, REPEAT('A',8101)); -- // Off-Page 저장소

VARCHAR 타입은 인덱스를 생성할 수 있는 반면 LOB 타입은 인덱스 생성을 할 수 없다는 이야기를 하는 사람도 있지만, 사실은 둘다 최대 크기 길이 제한만 충족시켜 주면 인덱스를 생성 할 수 있어요.

-- // 컬럼 그대로 사용시, 인덱스 생성 불가
mysql> ALTER TABLE tb_varchar ADD INDEX ix_fd (fd);
ERROR 1071 (42000): Specified key was too long; max key length is 3072 bytes

mysql> ALTER TABLE tb_lob ADD INDEX ix_fd (fd);
ERROR 1170 (42000): BLOB/TEXT column 'fd' used in key specification without a key length

-- // 컬럼 값의 길이(프리픽스)를 지정하면, 인덱스 생성 가능
mysql> ALTER TABLE tb_varchar ADD INDEX ix_fd ( fd(50) );
mysql> ALTER TABLE tb_lob ADD INDEX ix_fd ( fd(50) );

B-Tree 인덱스뿐만 아니라 전문 검색 인덱스도 TEXT 타입과 VARCHAR 타입 컬럼 모두 동일하게 생성할 수 있어요. 보면 볼수록 TEXT와 VARCHAR의 차이가 명확해지기 보다는 오히려 모호해지고 있다는 느낌일 거에요. 도대체 TEXT 컬럼과 VARCHAR 컬럼의 차이는 무엇이며, 어떤 경우에 TEXT 타입을 사용하고 어떤 경우에 VARCHAR 타입을 사용해야 할까요 ?

 

VARCHAR와 TEXT의 메모리 활용

MySQL 서버는 스토리지 엔진과 Handler API를 이용해서 데이터를 주고 받는데, 이때 MySQL 엔진과 InnoDB 스토리지 엔진은 uchar* records[2] 메모리 포인터를 이용해서 레코드 데이터를 주고 받아요. 이때 records[2] 메모리 객체는 실제 레코드의 데이터 크기에 관계 없이 최대 크기로 메모리를 할당해둬요. VARCHAR 타입은 최대 크기가 설정되기 때문에 메모리 공간을 records[2] 버퍼에 미리 할당받아둘 수 있지만, TEXT나 BLOB와 같은 LOB 컬럼 데이터의 경우 실제 최대 크기만큼 메모리를 할당해 두면 메모리 낭비가 너무 심해지는 문제가 있어요. 그래서 records[2] 포인터가 가리키는 메모리 공간은 VARCHAR는 포함하지만 TEXT 컬럼을 위한 공간은 포함하지 않아요.

uchar* records[2] 메모리 공간은 TABLE 구조체(struct) 내에 정의되어 있으며 TABLE 구조체는 MySQL 서버 내부에 캐싱되어서 여러 컨넥션에서 공유해서 사용될 수 있도록 구현되어 있어요. 즉, records[2] 메모리 버퍼는 처음 한번 할당되면 많은 컨넥션들에 의해서 재사용될 수 있도록 설계된 것이에요.
하지만 TEXT나 BLOB과 같은 LOB 컬럼을 위한 메모리 공간은 records[2]에 미리 할됭되어 있지 않기 때문에 매번 레코드를 읽고 쓸 때마다 필요한 만큼 메모리가 할당되어야 해요.

예를 들어서 아래와 같은 테이블을 생성했다면,

CREATE TABLE tb_lob (
  id INT PRIMARY KEY,
  fd TEXT
);

CREATE TABLE tb_varchar1 (
  id INT PRIMARY KEY,
  fd VARCHAR(100)
);

CREATE TABLE tb_varchar2 (
  id INT PRIMARY KEY,
  fd VARCHAR(10000)
);

tb_lob 테이블을 위한 records[2] 버퍼 공간은 16 * 2 바이트만큼 할당되고, tb_varchar1 테이블의 records[2] 버퍼 공간으로는 408 * 2 바이트를 할당해요. 그리고 마지막 tb_varchar2 테이블을 위해서는 40008 * 2 바이트를 할당해요.

  • tb_lob 테이블은 INT 타입의 컬럼(id)을 위한 4 바이트와 TEXT 값을 위한 포인터 공간 8바이트 그리고 헤더 공간 4바이트
  • tb_varchar1 테이블은 INT 타입의 컬럼(id)을 위한 4 바이트와 VARCHAR(100)타입 컬럼을 위한 공간 400바이트 그리고 헤더 공간 4바이트
  • tb_varchar2 테이블은 INT 타입의 컬럼(id)을 위한 4 바이트와 VARCHAR(10000) 타입 컬럼을 위한 공간 40000바이트 그리고 헤더 공간 4바이트

그래서 VARCHAR 타입의 컬럼을 읽을 때는 새롭게 메모리를 할당받는 것이 아니라 TABLE 구조체의 records[2] 버퍼를 이용해요. 하지만 TEXT나 BLOB와 같은 LOB 타입의 컬럼을 읽을 때는 (미리 할당해 둔 메모리 공간이 없기 때문에) 매번 필요한 크기만큼 메모리를 할당해서 사용후 해제해야 해요. LOB 컬럼의 값을 읽기 위해서 할당 및 해제하는 메모리 공간은 Performance_schema에 의해서 측정되지 않아요 (MySQL 8.0.33 기준). 그래서 LOB용 메모리 할당 해제가 실행되는지 알 수 없어서 성능 영향도를 파악하기가 어려운 상황이에요. 한가지 더 주의해야 할 것은 VARCHAR 타입에 저장된 값의 길이가 길어서 Off-Page로 저장된 경우, MySQL 서버는 TABLE 객체의 records[2] 버퍼를 사용하지 못하고 새롭게 메모리 공간을 할당해서 사용해요. 그래서 VARCHAR 타입에 매우 큰 값이 빈번하게 저장되는 경우는 주의가 필요해요.

 

컬럼 타입 선정 규칙

MySQL 서버의 내부적인 작동에서, VARCHAR와 TEXT 타입의 큰 차이점을 살펴보았어요. 지금까지 살펴본 내용을 토대로 VARCHAR나 TEXT 타입을 선택하는 규칙을 다음과 같이 정리해 볼 수 있어요.

VARCHAR

  • 최대 길이가 (상대적으로) 크지 않은 경우
  • 테이블 데이터를 읽을 때 항상 해당 컬럼이 필요한 경우
  • DBMS 서버의 메모리가 (상대적으로) 충분한 경우

TEXT

  • 최대 길이가 (상대적으로) 큰 경우
  • 테이블에 길이가 긴 문자열 타입 컬럼이 많이 필요한 경우
  • 테이블 데이터를 읽을 때 해당 컬럼이 자주 필요치 않은 경우

상대적이라는 단어가 많이 사용된 것은 DBMS 서버의 스펙이나 데이터 모델 그리고 유입되는 트래픽에 따라서 미치는 영향도가 다르기 때문이에요. 뿐만 아니라 DBMS 서버의 튜닝은 생산성(속도)과 효율성 사이에서 최적점(sweet-spot)을 찾는 과정이기 때문에 숫자 값 하나를 모든 판단의 기준으로 정하는 것은 불가능해요.

회사에서 Service Trust 라는 이름의 프로젝트를 진행한다.

거기에 나도 같이 참여하여 서비스의 신뢰성을 높이게 되었다.


기준을 어떻게 가져갈까 하다가 딜리버리 히어로의 신뢰성 선언문을 보았다.

 

인상적인 내용 몇 개만 정리해본다.

내용 중 핵심에 진하게 표시하여 나타냈다.


아키텍처 및 기술 부채

A-1 아키텍처를 문서화합니다. 모든 서비스와 해당 종속성은 아키텍처 가이드라인에 설명된 규칙에 따라 아키텍처 차트에 문서화되어야 합니다.

A-2 우리는 이름 지정에 신경을 씁니다. 모든 서비스에는 명확하고 이해하기 쉬운 이름이 있어야 합니다. 

A-3 모놀리스로 시작합니다. 모놀리스 첫 번째 패턴을 따르면 빠르게 움직일 수 있으며 나중에 요구 사항에 대한 더 나은 지식을 갖게 되면 하위 서비스로 나눌 수 있습니다. 

A-4 우리는 마이크로서비스 원칙을 수용합니다 . 새로운 서비스는 다음을 충족해야 합니다.

  • API나 메시지를 통해서만 통신하세요.
  • 다른 서비스와 데이터 저장 공간을 공유하지 마세요.
  • 기술적 구성 요소가 아닌 제품 도메인(제한된 컨텍스트)의 일부가 되어야 합니다. 
  • 단일 책임 원칙을 따르고 한 가지 일을 잘 수행하십시오.
  • 단 하나의 분대만이 소유할 수 있습니다. 
  • 2개 서비스보다 깊은 동기식 호출 체인을 피하세요. 
  • 데이터와 도구를 사용하여 서비스의 상태와 동작을 관찰하여 사고로부터 복원하는 데 걸리는 시간을 최소화합니다.
  • 오버헤드를 정당화할 수 있을 만큼 충분한 크기를 갖습니다(나노 서비스 없음).
  • 데이터 일관성 보장(예: 최소 1회 메시지 전달)을 전달합니다.

A-5 우리는 Data mess를 수용합니다. 데이터 생산자는 자신의 도메인 데이터를 공통 데이터 플랫폼(Datahub)에 제품으로 게시할 책임이 있으며 다음 사항을 보장합니다.

  1. 검색 가능.
  2. 주소 지정 가능.
  3. 신뢰할 수 있고 진실합니다.
  4. 자기 설명적 의미와 구문.
  5. 상호 운용 가능하며 글로벌 표준에 따라 관리됩니다.
  6. 글로벌 액세스 제어로 안전하고 관리됩니다.

A-6 아키텍처 검토: 새로운 서비스는 부족 및 수직 기술 리더와의 기술 설계 검토 회의를 통과해야 합니다. 부족의 기술 리더는 2년에 한 번씩 전체 아키텍처를 동료에게 발표합니다. 두 회의 모두 관심 있는 누구에게나 열려 있습니다. 

A-7 우리는 부채를 알고 있습니다 . 분기별로 기술 부채에 동의하고 추적합니다.

A-9 우리는 중요한 사항을 최소화합니다. 우리는 서비스가 주문 이행을 위한 중요한 경로의 일부가 되는 것을 피합니다. 중요하지 않은 범위를 다른 곳으로 이동하여 중요한 서비스를 간결하게 유지합니다.

A-10 우리는 결정을 문서화합니다. 우리는 RFC  아키텍처 결정 기록을 사용하여 중요한 아키텍처 결정을 그 맥락 및 결과와 함께 조정하고 문서화합니다.

A-11 우리는 잘 정의된 API를 좋아합니다. 우리는 다른 팀에 공개된 모든 API를 문서화하고 API 라이브러리에 문서를 게시합니다.

배달

D-1 매일 배포합니다 . 우리의 목표는 근무일 기준으로 엔지니어당 최소 한 번의 프로덕션 배포를 수행하는 것입니다.

D-2 배포 추적 : 모든 배포는 배포 이벤트를 통해 추적되어야 합니다.

D-3 연중무휴 24시간 운영: 모든 서비스는 가동 중지 시간 없는 배포를 지원해야 합니다. 피크 시간대와 금요일 에는 배포가 안전합니다.

D-4 안전망을 사용하여 배포 : 모든 Tier 1 서비스는 자동화된 카나리아 배포를 지원해야 합니다.

D-5 우리의 인프라는 코드입니다 . 모든 인프라는 코드와 저장소에 구성되어야 합니다. 모든 인프라 변경 사항은 CI/CD 파이프라인을 통해 실행됩니다.

D-6 우리는 테스트를 자동화합니다 . 우리는 프로덕션 코드가 단위 테스트를 통해 60-80%의 코드 적용 범위를 가질 것으로 기대합니다. 또한 주요 애플리케이션 흐름은 통합 테스트를 통해 다루어져야 합니다.

D-7 우리는 절대 혼자 코딩하지 않습니다 . 새롭고 복잡한 코드의 경우 쌍 프로그래밍을 사용합니다. 모든 프로덕션 코드 변경 사항은 코드 검토를 거칩니다.

우리는 실패를 고려합니다.

더보기

실패 처리 전략

- Timeout: Beyond a certain wait interval, a successful result is unlikely or simply too late.
- Retry: Many faults are transient and may self-correct after a short delay – use exponential backoff and jitter to avoid cascading failures.
- Circuit Breaker: When a system is seriously struggling, failing fast is better than making clients wait to avoid running out of critical resources.
- Fallback: Things will still fail – plan what you will do when that happens.
- Throttling: Prevent misbehaving clients bringing your service down.
We prepare for rapid growth with ongoing load-tests:ts bringing your service down.
- Idempotence: Multiple identical requests made to a service apply only once.
- Recoverable: Your service has a process to replay messages to recover from an outage of a downstream service.
- Dead-letter queues: Erroneous messages do not stop a service processing valid messages and are published to a dead-letter queue for later analysis.


https://tech.deliveryhero.com/our-reliability-manifesto/

 

https://tech.deliveryhero.com/our-reliability-manifesto/

Our Reliability Manifesto is a succinct collection of rules, guidelines, and best practices that reflect our current thinking on what it takes to build a reliable system. At Delivery Hero, we solve complex challenges at scale – from how to get an ice cre

tech.deliveryhero.com

 

'개발 관심사' 카테고리의 다른 글

MSA 관련 자료집  (0) 2022.04.13
웹 서비스 서버 성능 지표들  (0) 2022.02.21
Gradle과 Maven  (0) 2022.02.16
Apache Log4j 원격코드 실행 취약점 (cve-2021-44228)  (0) 2021.12.14

양자화 머고, 왜 하는 걸까


양자화란 연속적으로 보이는 양을 자연수로 셀 수 있는 양으로 재해석하는 과정이라고 한다. 기본적으로 용량이나 계산량을 줄이는 것이다. 
현실에서 예를 들면, 현실의 연속적으로 보이는 순간을 비디오로 찍으면 비디오 찍으면 비디오의 픽셀 단위로 각 색상을 인식해 영상화 한다. 그 과정에서 연속적인 현실의 모습이 자연수로 셀 수 있는 양으로 양자화되어 비디오로 만들어진다.

 

 

기본적으로, 데이터 손실에 해당한다. 그렇지만 우리의 머니와 시간은 한정되어 있기 때문에, 연산량과 저장량을 줄이기 위해 필수적으로 수행해야한다.

 

양자화 코드 예시

import yaml

from furiosa.models.vision import SSDMobileNet
from furiosa.quantizer import quantize
from furiosa.runtime.sync import create_runner

image = ["image path"]

mobilenet = SSDMobileNet()
onnx_model = mobileNet.origin
calib_range = mobileNet.tensor_name_to_range

quantized_onnx = quantize(onnx_model, calib_range)

with create_runner(quantized_onnx, devic="warbox*1") as runner:
    inputs, contexts = mobilenet.preprocess(image)
    outputs = runner.run(inputs)
    mobilenet.postprocess(outputs, contexts[0])

CPU보다 GPU가 연산이 빠르다만 알고 있지 말고, 왜 그런 지 자세히 알아보자

 


🚀 CPU와 GPU 내부 구조

CPU와 GPU는 둘 다 데이터를 읽어들여 연산처리를 통해 답을 도출하는 기능을 수행합니다. 컴퓨터에서의 두뇌 역할을 한다는 점에서는 비슷합니다. 다만, 프로세서 내부의 구조를 살펴보면 CPU와 GPU는 차이가 큽니다.

CPU와 GPU 같은 프로세서 내부는 크게 연산을 담당하는 산출연산처리장치(ALU)와 명령어를 해석하고 실행하는 컨트롤유닛(CU), 각종 데이터를 담아두는 캐시로 나뉘게 됩니다.

 

CPU

ALU 이외에도 맥락을 저장할 수 있도록 뇌의 형태를 카피한 형태로 되어 있다. 기본적으로 DRAM과 결합하여 컴퓨팅을 수행할 수 있도록 구성되어 있다. 일반적으로 ALU (core) 의 수와 DRAM 혹은 cache 의 성능과 비례한다. 계산만을 수행하기 위한 기기는 아니고, 전체 job을 컨트롤 하며 인간이 이해할 수 있는 프로그래밍 언어를 컴퓨터의 언어 (0101010 이진수로 된 명령어)로 풀어내서 수행하는 연산을 내부적으로 수행한다. 대부분의 작업은 순차적으로 이루어진다.

 

GPU

ALU 머신이다. 계산을 병렬적으로 하기 위해 설계 되었다. 캐시메모리를 두어 계산하는 형태보다는 연산만을 위한 기기이다.

더 자세한 GPU의 아키텍처는 다음 링크를 참고해라

https://www.techspot.com/article/2151-nvidia-ampere-vs-amd-rdna2/

🪁 CPU와 GPU 쓰임새

이렇듯 CPU와 GPU는 그 구조가 다른만큼 쓰임새도 다릅니다.

쓰임새를 알기 전에 컴퓨터가 데이터를 저장하는 방법에 대해 알아야 합니다.

  • 정수 : 0과 1의 수로 구분하여 저장
  • 실수 : 고정소수점 혹은 부동소수점이라는 데이터 저장방식으로 저장
    • 고정소수점(fixed point) : C#에서 decimal가 고정소수점 방식. 정밀한 표현이 가능하지만 표현할 수 있는 범위가 적습니다.
    • 부동소수점(floating point) : C#에서 float, double가 부동소수점 방식. 훨씬 더 넓은 범위까지 표현할 수 있지만 오차가 발생합니다.


CPU보다 GPU를 사용해야 하는 경우 (AWS 설명)

CPU와 그래픽 처리 장치(GPU) 사이에서 선택할 때 서로 배타적이지 않다는 점에 유의해야 합니다. 클라우드의 모든 서버 또는 서버 인스턴스를 실행하려면 CPU가 필요합니다. 그러나 일부 서버에는 GPU를 추가 코프로세서로 포함하기도 합니다. 특정 워크로드는 특정 함수를 보다 효율적으로 수행하는 GPU가 있는 서버에서 실행하는 것이 더 적합합니다. 예를 들어, GPU는 부동 소수점 숫자 계산, 그래픽 처리 또는 데이터 패턴 매칭에 유용할 수 있습니다.

다음은 CPU보다 GPU를 사용하는 것이 더 유용한 몇 가지 분야입니다.

딥 러닝

딥 러닝은 인간의 두뇌에서 영감을 얻은 방식으로 데이터를 처리하도록 컴퓨터를 가르치는 인공 지능(AI) 방식입니다. 예를 들어, 딥 러닝 알고리즘은 그림, 텍스트, 사운드 및 기타 데이터의 복잡한 패턴을 인식하여 정확한 인사이트와 예측을 생성할 수 있습니다. GPU 기반 서버는 기계 학습, 신경망 및 딥 러닝 작업에 고성능을 제공합니다.

딥 러닝에 대해 읽어보기 »

기계 학습에 대해 읽어보기 »

신경망에 대해 읽어보기 »

고성능 컴퓨팅

고성능 컴퓨팅이라는 용어는 매우 뛰어난 컴퓨팅 성능이 필요한 작업을 의미합니다. 다음은 몇 가지 예입니다.

  • 지구과학 시뮬레이션과 지진 처리를 빠르게 대규모로 실행해야 하는 경우
  • 제품 포트폴리오 위험, 헤지 기회 등을 식별하도록 재무 시뮬레이션을 계획해야 하는 경우
  • 의학, 유전체학 및 신약 개발 분야에서 예측, 실시간 또는 회귀적 데이터 과학 애플리케이션을 구축해야 하는 경우

GPU 기반 컴퓨터 시스템은 이와 같은 고성능 컴퓨팅 작업에 더 적합합니다.

고성능 컴퓨팅에 대해 읽어보기 »

자율 주행 교통 수단

고급 운전자 지원 시스템(ADAS)과 자율 주행 교통 수단(AV) 시스템을 개발하고 배포하려면 확장성이 뛰어난 컴퓨팅, 스토리지, 네트워킹 및 분석 기술이 필요합니다. 예를 들어, 데이터 수집, 레이블링 및 주석, 지도 개발, 알고리즘 개발, 시뮬레이션 및 검증을 위한 기능이 필요합니다. 이러한 복잡한 워크로드에서는 효율적인 연산을 위해 GPU 기반 컴퓨터 시스템의 지원이 필요합니다.

 

  CPU 그래픽 처리 장치(GPU)
함수 서버의 주요 처리 연산을 해결하는 일반화된 구성 요소 병렬 컴퓨팅에 탁월한 전문화된 구성 요소
처리 중 직렬 명령 처리를 위해 설계됨 병렬 명령 처리를 위해 설계됨
설계 코어 수는 더 적지만 코어 성능은 더 강력함 CPU보다 코어 수는 더 많지만 CPU 코어보다 성능이 떨어짐
가장 적합한 용도 범용 컴퓨팅 애플리케이션 고성능 컴퓨팅 애플리케이션



참고
CPU vs GPU nvidia

https://www.youtube.com/watch?v=-P28LKWTzrI

https://velog.io/@ckstn0777/%EC%BB%B4%ED%93%A8%ED%84%B0%EA%B5%AC%EC%A1%B0-CPU%EC%99%80-GPU

 

CPU와 GPU

CPU와 GPU는 둘 다 데이터를 읽어들여 연산처리를 통해 답을 도출하는 기능을 수행합니다. 컴퓨터에서의 두뇌 역할을 한다는 점에서는 비슷합니다. 다만, 프로세서 내부의 구조를 살펴보면 CPU와 G

velog.io

https://aws.amazon.com/ko/compare/the-difference-between-gpus-cpus/

서버리스(Serverless)란 무엇인가?

서버(Server) + 리스(Less)의 합성어라 간혹 '서버가 없다'라고 문자 그대로 이해할 수 있지만, 절대 그렇지 않다.

서버리스(Serverless)는 클라우드 컴퓨팅의 모델 중 하나로 개발자가 서버를 직접 관리할 필요가 없는 아키텍처를 의미한다.

예를들어, 서버의 사용자가 1000명이 될 걸 예상하고 그에 맞는 용량의 서비스를 구입했다면 실제 사용자가 1000명이든 0명이든 같은 금액을 내야 할 것이다. 이는 크던 작던 손실을 일으키기 마련이다.

하지만 서버리스는 동적으로 서버의 자원을 할당한다.

사용자가 없다면 자원을 할당하지 않고 대기하다 요청이 들어오면 그 때 자원을 할당해서 요청을 처리하고 다시 대기 상태로 들어가게 된다.
즉, 자원을 효율적으로 사용할 수 있는 것이다.
비용 또한 대기상태를 제외한 실제 사용 자원에 대해서만 청구되기 때문에 굉장히 경제적이며,
해당 서버는 클라우드 제공 기업에서 전적으로 관리하기 때문에 개발자는 스케일링, 업데이트, 백업, 보안 등 서버에 대해 일절 관리하거나 신경 쓸 필요가 없어 비즈니스 로직에 집중하여 개발을 할 수 있다.

서버리스 아키텍처(Serverless Architecture)의 구현 방식

서버리스 아키텍처의 대표적인 두 가지 구현 방식은 다음과 같다.

FaaS (Function as a Service) : AWS Lambda, Microsoft Azure Function, Google Cloud Functions ... 등
BaaS (Backend as a Service) : Firebase, Kinvey, Parse ... 등
두 가지 구현 방식으로 나뉘지만, 일반적으로 서버리스라고 하면 FaaS를 가리킨다.

FaaS(Function as a Service)

FaaS는 Function 즉, 함수를 서비스로 제공한다.

사용자가 작성한 코드(백엔드)를 서버리스 제공자의 서버에 업로드하면 해당 서버는 업로드한 코드를 함수 단위로 쪼개 대기상태로 두게된다.
그러다 요청이 들어오면 서버가 대기상태에 두었던 함수를 실행시켜 처리한 후 작업이 끝나면 다시 대기상태로 만드는 구조이다.
비용은 함수 호출 횟수에 따라 청구된다.

쉽게 말해 업로드한 코드는 평소에 자고있고, 요청이 들어오면 서버가 코드를 깨워 요청을 처리한 후 다시 재운다.

FaaS의 특징

  1. Stateless

FaaS 서비스는 함수가 실행되는 동안에만 관련된 자원을 할당하게 되며, 함수가 항상 같은 머신에서 실행된다는 보장이 없다. 따라서 함수 실행 시 로컬에서 어떤 상태(State)가 유지될 수 없다.
이를 해결하고 싶다면 메모리에 상태를 저장하지 않고 AWS의 경우 S3을 이용하거나 아예 DB를 사용해야 한다.

  1. Ephemeral

위에서 말한 것과 같이 함수는 특정 이벤트가 발생했을 때만 컨테이너로서 배포되고, 실행이 끝난 후엔 자원이 회수되므로 일시적으로만 배포된다고 할 수 있다.

BaaS (Backend as a Service)

BaaS는 일반적으로 SPA, 안드로이드와 같은 클라이언트 중심으로 개발된 어플리케이션이다.

클라이언트단에서 BaaS가 제공하는 인증, DB, 사용자 관리 등과 같은 백엔드 관련 외부 서비스를 사용해서 대부분의 비즈니스 로직을 처리한다.

쉽게말해 SNS연동이나 DB와 같이 백엔드에 필요한 기능들을 사용자가 직접 구현할 필요 없이 제공하는 API로 해당 기능을 구현할 수 있게 해주는 것이다.
클라우드 공급자가 백엔드 개발 환경까지 제공해준다고 보면 될 것 같다.

누가 BaaS를 사용해야 할까?
BaaS 플랫폼은 기술적인 서비스이고, 앱 개발자들을 위해 만들어졌다.

  • Backend 개발에 있어 한정된 지식을 가진 Frontend 개발자
  • 빠른 개발을 원하는 Backend 개발자

어떤 유형의 프로젝트가 BaaS와 잘 어울릴까?

  • 실시간 응용프로그램(채팅, 메시지 앱)
  • 교통 앱
  • 소셜-네트워크 유형 앱
  • 전자상거래 앱
  • 음악 또는 영상 스트리밍 앱
  • 게임

서버리스(Serverless)의 장단점

장점

기존 IaaS나 PaaS와는 다르게 실제 사용량에 대해서만 비용이 청구되므로 경제적
서버에 신경 쓸 필요가 없으므로 개발자 생산성 향상 및 개발 시간 단축
자동 스케일 업 및 스케일 다운
간단한 패키징 및 배포

단점

Cold Start
서버가 항시 요청에 대기하고 있는게 아니라 느리다. 프로젝트 규모가 작다면 크게 신경쓸만한 사항은 아니지만 규모가 커지거나 속도를 요구하는 프로젝트라면 서버리스는 좋은 선택이 아닐 수 있다.

실행 시간 한계
서버리스는 단순 작업(댓글 쓰기, 이메일 보내기 등)에는 적합하지만 긴 시간이 필요한 작업(동영상 업로드, 데이터 백업 등)에는 굉장히 비효율적이다. 서버리스는 함수가 1회 호출 될 때 사용할 수 있는 메모리 및 시간에 제한이 있기 때문이다. 만약 작업이 끝나지 않은채로 해당 시간이 지나면 작업이 완료될때까지 일정 시간마다 계속 함수를 다시 호출하게 된다.
클라우드 제공 플랫폼에 종속적

디버깅 및 테스트 불편
서버리스(Serverless)를 추천하는 대상
서버리스는 사이드 프로젝트(토이 프로젝트)나 빠르게 시제품(초기 서비스)을 런칭하고 싶은 경우에 추천한다.
쉽고 빠르게 제품을 출시할 수 있으며, 돈도 매우 절약된다.

안녕하세요 임승현의 이력서입니다 😀

크게 보고 싶으신 분들은 PDF 다운로드해서 봐보시면 좋을 거 같습니다.

프로덕트를 만들가는 메이커, 임승현 68b4f85dab85460780be670f091f6326.pdf
0.55MB

 

 

오랜만에 주말 풀이~

스케쥴링 구현 문제였고 DP 메모라이제이션을 넣어서 풀었다.


문제 설명

https://school.programmers.co.kr/learn/courses/30/lessons/214288

문제 설명 궁금하면 아래 더보기 누르기

더보기

현대모비스는 우수한 SW 인재 채용을 위해 상시로 채용 설명회를 진행하고 있습니다. 채용 설명회에서는 채용과 관련된 상담을 원하는 참가자에게 멘토와 1:1로 상담할 수 있는 기회를 제공합니다. 채용 설명회에는 멘토 n명이 있으며, 1~k번으로 분류되는 상담 유형이 있습니다. 각 멘토는 k개의 상담 유형 중 하나만 담당할 수 있습니다. 멘토는 자신이 담당하는 유형의 상담만 가능하며, 다른 유형의 상담은 불가능합니다. 멘토는 동시에 참가자 한 명과만 상담 가능하며, 상담 시간은 정확히 참가자가 요청한 시간만큼 걸립니다.

참가자가 상담 요청을 하면 아래와 같은 규칙대로 상담을 진행합니다.

  • 상담을 원하는 참가자가 상담 요청을 했을 때, 참가자의 상담 유형을 담당하는 멘토 중 상담 중이 아닌 멘토와 상담을 시작합니다.
  • 만약 참가자의 상담 유형을 담당하는 멘토가 모두 상담 중이라면, 자신의 차례가 올 때까지 기다립니다. 참가자가 기다린 시간은 참가자가 상담 요청했을 때부터 멘토와 상담을 시작할 때까지의 시간입니다.
  • 모든 멘토는 상담이 끝났을 때 자신의 상담 유형의 상담을 받기 위해 기다리고 있는 참가자가 있으면 즉시 상담을 시작합니다. 이때, 먼저 상담 요청한 참가자가 우선됩니다.

참가자의 상담 요청 정보가 주어질 때, 참가자가 상담을 요청했을 때부터 상담을 시작하기까지 기다린 시간의 합이 최소가 되도록 각 상담 유형별로 멘토 인원을 정하려 합니다. 단, 각 유형별로 멘토 인원이 적어도 한 명 이상이어야 합니다.

예를 들어, 5명의 멘토가 있고 1~3번의 3가지 상담 유형이 있을 때 아래와 같은 참가자의 상담 요청이 있습니다.

참가자의 상담 요청

참가자 번호시각상담 시간상담 유형

1번 참가자10분60분1번 유형
2번 참가자 15분 100분 3번 유형
3번 참가자 20분 30분 1번 유형
4번 참가자 30분 50분 3번 유형
5번 참가자 50분 40분 1번 유형
6번 참가자 60분 30분 2번 유형
7번 참가자 65분 30분 1번 유형
8번 참가자 70분 100분 2번 유형

이때, 멘토 인원을 아래와 같이 정하면, 참가자가 기다린 시간의 합이 25로 최소가 됩니다.

1번 유형2번 유형3번 유형

2명1명2명

1번 유형

1번 유형을 담당하는 멘토가 2명 있습니다.

  • 1번 참가자가 상담 요청했을 때, 멘토#1과 10분~70분 동안 상담을 합니다.
  • 3번 참가자가 상담 요청했을 때, 멘토#2와 20분~50분 동안 상담을 합니다.
  • 5번 참가자가 상담 요청했을 때, 멘토#2와 50분~90분 동안 상담을 합니다.
  • 7번 참가자가 상담 요청했을 때, 모든 멘토가 상담 중이므로 1번 참가자의 상담이 끝날 때까지 5분 동안 기다리고 멘토#1과 70분~100분 동안 상담을 합니다.

2번 유형

2번 유형을 담당하는 멘토가 1명 있습니다.

  • 6번 참가자가 상담 요청했을 때, 멘토와 60분~90분 동안 상담을 합니다.
  • 8번 참가자가 상담 요청했을 때, 모든 멘토가 상담 중이므로 6번 참가자의 상담이 끝날 때까지 20분 동안 기다리고 90분~190분 동안 상담을 합니다.

3번 유형

3번 유형을 담당하는 멘토가 2명 있습니다.

  • 2번 참가자가 상담 요청했을 때, 멘토#1과 15분~115분 동안 상담을 합니다.
  • 4번 참가자가 상담 요청했을 때, 멘토#2와 30분~80분 동안 상담을 합니다.

상담 유형의 수를 나타내는 정수 k, 멘토의 수를 나타내는 정수 n과 참가자의 상담 요청을 담은 2차원 정수 배열 reqs가 매개변수로 주어집니다. 멘토 인원을 적절히 배정했을 때 참가자들이 상담을 받기까지 기다린 시간을 모두 합한 값의 최솟값을 return 하도록 solution 함수를 완성해 주세요.

 


제한사항

  • 1 ≤ k ≤ 5
  • k ≤ n ≤ 20
  • 3 ≤ reqs의 길이 ≤ 300
    • reqs의 원소는 [a, b, c] 형태의 길이가 3인 정수 배열이며, c번 유형의 상담을 원하는 참가자가 a분에 b분 동안의 상담을 요청했음을 의미합니다.
    • 1 ≤ a ≤ 1,000
    • 1 ≤ b ≤ 100
    • 1 ≤ c ≤ k
    • reqs는 a를 기준으로 오름차순으로 정렬되어 있습니다.
    • reqs 배열에서 a는 중복되지 않습니다. 즉, 참가자가 상담 요청한 시각은 모두 다릅니다.

입출력 예

3 5 [[10, 60, 1], [15, 100, 3], [20, 30, 1], [30, 50, 3], [50, 40, 1], [60, 30, 2], [65, 30, 1], [70, 100, 2]] 25
2 3 [[5, 55, 2], [10, 90, 2], [20, 40, 2], [50, 45, 2], [100, 50, 2]] 90

풀이 방법

1번

각 유형별로 req를 모아둔다.

2번

각 유형별로 멘토가 배정될 수 있는 모든 경우의 수를 가지고 for문을 돌린다.
중복 조합을 사용하였다.

5명 멘토 /.3개 과제

3 1 1
1 2 2
1 3 1
2 1 1
2 1 2

3번

각 과제에 멘토가 배정된 경우일 때 스케쥴링을 돌린 후 기다린 시간의 총합을 계산한다.

4번

각 과제에 멘토가 배정된 수에 따라 기다린 시간의 총합을 일정하므로 그 값을 재활용할 수 있게 dp map에 저장해서 쓴다.

1번 과제에 1명의 멘토가 붙는 경우
1번 과제에 2명의 멘토가 붙는 경우

요런 값들은 기다린 시간의 총합이 정해져있기 때문에 다음 계산 시에도 사용 가능하다.

풀이 코드


from itertools import combinations_with_replacement
from heapq import heappush, heappop
from collections import deque

dp = {}

def solution(k, n, reqs):
    answer = 300 * 1000 + 1

    reqs_by_type = [[] for i in range(k+1)]
    for req in reqs:
        reqs_by_type[req[2]].append(req)
    for req in reqs_by_type:
        req.sort(key = lambda x: x[0])

    if (n == k):
        return get_total_waiting_time(reqs_by_type, [1] * (k+1))

    # 배정한 거 다 돌려보기
    for cwr in combinations_with_replacement(list(range(1, k+1)), n-k):
        mentor_counts = [1] * (k+1)
        for c in cwr:
            mentor_counts[c] += 1
        answer = min(answer, get_total_waiting_time(reqs_by_type, mentor_counts))
    return answer

def get_total_waiting_time(reqs_by_type, mentor_counts) :
    time = 0
    for type_num, m in enumerate(mentor_counts):
        if type_num == 0 : continue
        time += get_waiting_time_by_type(reqs_by_type[type_num], type_num, m)
    return time

def get_waiting_time_by_type(reqs, type_num, mentor_count):
    if not reqs : return 0

    key = (type_num, mentor_count)
    if key not in dp:
        waiting_time = 0
        heap = [(0, i) for i in range(mentor_count)]

        reqs = deque(reqs[:])
        cur_time = 0
        while reqs:
            end_time, mentor_idx = heappop(heap)
            cur = reqs.popleft()
            cur_time = max(cur[0], end_time)
            waiting_time += max(0, end_time - cur[0])
            heappush(heap, (cur_time + cur[1], mentor_idx))
        dp[key] = waiting_time
    return dp[key]

'알고리즘' 카테고리의 다른 글

My github as algorithm hub  (0) 2021.06.16
[이코테] 구현 - 게임 개발  (0) 2021.06.13
[stack] 2504번 괄호의 합  (0) 2019.06.03

+ Recent posts