ProxySQL은 MySQL 호환 데이터베이스(MySQL, Persona, MariaDB 등)를 backend로 두고, 외부의 connection을 받아 쿼리를 중계해 주는 쿼리 라우팅(Query routing)을 핵심으로 하는 MySQL Proxy Database다. 설정되어 있는 backend들에 주기적으로 health check를 보내고, 이들 중 하나가 죽으면 auto failover해주는 등, 로드 밸런싱과 유사하다. 이 외에도 쿼리 캐싱, 다운타임 없는 설정 변경 등을 지원한다. Amazon RDS 등에서 이야기하는 Master/Slave 구조로 인해 데이터베이스가 많은 수로 read replication되어 있는 상태에서 이들에게 효율적으로 커넥션을 분배해 주거나, 외부에서 많은 양의 커넥션이 들어오는 경우 효과적으로 connection을 pooling하기 위해 자주 사용된다.

구성

ProxySQL은 Runtime, Memory, Disk, Config file의 4가지 계층으로 이루어져 있다. 어느 곳에서 영감을 받았는진 모르겠지만, 처음 봤을 땐 꽤 생소하다.

Runtime

ProxySQL로 들어오는 요청을 처리하는 스레드의 메모리 내 데이터 구조를 표현한다. 시스템 수준에서 디버깅을 하는 정도가 아니라면 비교적 신경쓸 일이 적다.

Memory

main이라고도 부른다. MySQL 호환 인터페이스를 사용할 수 있는, in-memory로 동작하는 데이터베이스를 나타낸다. 쉽게 말하면 여기서 설정 값이 상주하고 있고, MySQL 클라이언트로 쿼리 가능하다는 의미다. Memory 내에는 대표적으로 아래 4개의 테이블이 존재한다.

  • mysql_servers : ProxySQL이 실제로 중계할 서버들의 목록을 관리한다. 중계 대상 서버들을 백엔드 서버라고도 부른다.
  • mysql_users : ProxySQL이 관리하는 사용자의 자격 증명 목록이다. '얘는 이 데이터베이스에 read, 얘는 이 데이터베이스에 read/write 권한, ..' 같은 설정들을 다룬다.
  • mysql_query_rules : 백엔드 서버로 트래픽이 라우팅될 때 평가되는 쿼리 규칙의 목록이다. 이 테이블을 사용하면, 'LIMIT 없는 쿼리는 사용하지 못한다' 와 같은 동작이 가능하다.
  • global_variables : 프록시가 사용하도록 구성된 전역 변수의 목록이며, MySQL connection timeout이나, connect retry 횟수와 같은 메타데이터들을 다룬다.

Disk

ProxySQL 자체 데이터베이스가 실제로 동작하는 곳의 이름이 memory인 이유는, 실제로 in-memory 형태의 SQLite3 데이터베이스가 상주하고 있기 때문이다. 따라서 영속성이 보장되지 않기 때문에, memory 내 구성을 유지하기 위한 계층으로서 Disk가 사용된다.

Config file

Memory에서 돌아가고 있는 데이터베이스에 설정 값들을 dump시킬 수 있는, 전형적인 설정 파일이다.

production level의 MySQL Proxy로 ProxySQL을 가장 많이 사용하고 있는 것 같다. 설치 과정은 ProxySQL Github의 wiki에 잘 나와 있다. 백엔드 인프라의 고도화 단계엔 정말 들어가는 게 많은 것 같다.

'배경지식' 카테고리의 다른 글

직렬화와 JSON  (0) 2019.02.12
ORM  (0) 2019.02.12
HTTP 메소드  (0) 2019.02.12
StatsD  (0) 2019.02.11
HTTP 헤더  (0) 2018.11.02

직렬화(serialization)은 어떠한 데이터 구조를 송/수신하거나 저장하기 위해 전송에 적합한 포맷으로 변환하는 과정이다. 대부분의 서비스에는 통신이 필요하고, 어떤 프로토콜을 쓰던 간에 컴포넌트 간 데이터 통신(안드로이드 앱 → Python 서버 등)을 위해서는 프로그래밍 언어나 플랫폼에 독립적인, 표준화된 포맷이 필요하다.(Java의 객체를 이런저런 방식으로 바이너리로 변환해 봤자, 다른 언어에는 이에 대한 표준이 없어서 완벽하게 읽을 수 없기 때문에) 따라서 직렬화는 컴퓨터 과학에서 매우 중요하다. 여기서는 데이터를 송/수신하기 위한 직렬화에 대해 이야기해 보겠다.

웬만한 서비스에서 대부분의 데이터는 'ID는 planb고, 나이는 19세다'처럼 a는 b같은 map 개념과 '학생 목록'처럼 학생1, 학생2, 학생3, ...같은 list 개념만 있으면 대충 다 표현할 수 있다. 예를 들어, '학생들의 주소록'은 '이름', '주소', '전화번호' 등이 담긴 각각의 요소를 list로 묶어주면 된다. 결론적으로 map과 list를 어떤 모습으로 표현할 것인지, 요소들에는 어떤 타입을 허용할지에 따라 수많은 형태의 직렬화 양식이 존재한다.

위부터 차례대로 json, xml, yaml이다. 이들 중 데이터의 송/수신에서 가장 많이 쓰이는 것은 (아마도)JSON이다.

JSON

JSON은 JavaScript Object Notation의 약자이며, 따라서 JavaScript의 객체를 표현하기 위한 포맷에서 파생되었다. key-value 쌍으로 이루어진 object 타입과, 값들의 순서화된 열거인 array 타입으로 나뉜다.

Object

Object는 Python의 Dictionary, Java의 HashMap과 대응할 수 있으며, 비순서화된 key-value 쌍으로 이루어진다. 한 object에서 key는 항상 유일성이 보장되며, 따라서 각각의 요소들을 key로 식별한다. 중괄호로 감싸고, key와 value의 구분자로 colon(:)을 사용하고, comma(,)로 각 쌍을 구분한다.

Array

Array는 Python의 List, Java의 ArrayList와 대응할 수 있으며, 값(value)의 순서화된 열거를 표현한다. 대괄호로 감싼다.

value

사실상 특정 언어에서 JSON이라는 데이터를 직렬화/역직렬화하기 위한 패키지를 개발하려면, 위에서 이야기한 format에 대한 정의와 함께 value의 정의도 필요하다. 문자열(string), 숫자(number), JSON object, JSON array, boolean 타입 또는 null이 올 수 있다.

결론적으로,

  • A와 B가 통신하려면 특정 프로그래밍 언어에 의존하지 않는 데이터 포맷이 아무튼 필요하다.
  • 이를 위해 많은 사람들이 직렬화 포맷들을 이것저것 제안했으나 결국은 JSON이 가장 많이 쓰인다.
  • 콤마로 구분된 key-value 매핑을 중괄호로 감싸고, 이를 'object'라고 부르며
  • 요소의 열거를 표현하기 위해 콤마로 구분된 값들을 대괄호로 감싸고, 이를 'array'라고 부른다.
  • 이 둘을 통해 열심히 데이터를 만들어서 송/수신하면, 대부분의 프로그래밍 언어가 JSON 파싱을 지원하기 때문에 알아서 잘 파싱해서 쓸 수 있다.
  • 따라서 클라이언트가 서버로 데이터를 쏠 때나, 서버가 클라이언트로 데이터를 내려줄 때나 웬만하면 JSON을 사용하는 것 같다.


'배경지식' 카테고리의 다른 글

ProxySQL  (1) 2019.02.12
ORM  (0) 2019.02.12
HTTP 메소드  (0) 2019.02.12
StatsD  (0) 2019.02.11
HTTP 헤더  (0) 2018.11.02

어플리케이션 레벨의 입장에서, raw string 형태의 SQL은 여간 귀찮은 일이 아닐 수가 없다. 수많은 객체 간의 작업, 또는 함수와 콜백 간의 인터랙션이 주가 되는 소스 코드에서, raw string으로 되어 있는 쿼리 문자열은 가독성에 문제를 주기도 하고, 테이블이 alter되거나 데이터베이스 엔진을 마이그레이션했다면 쿼리 문자열들을 하나하나 찾아 수정해 주어야 하는 문제점도 있다.

한가지 예를 들어보자. tbl_users라는 테이블에 사용자에 관련된 몇가지 컬럼들이 있어서, 특정 나이에 해당하는 사용자들의 리스트를 반환하는 함수를 작성한다고 가정하면, 아래와 같은 코드가 나올 수 있다.

위 코드는 그리 문제가 없어 보이지만, tbl_users 테이블에 대한 쿼리를 다루는 곳이 프로젝트 전체에 걸쳐 10군데가 넘는다고 생각해 보자.

  1. 테이블이 alter되어 email 컬럼이 user_email이라는 이름으로 변경되었다면, '이름 변경'이라는 단순한 작업임에도 불구하고 해당 테이블을 사용하는 쿼리의 갯수만큼 작업 공수가 높아진다.
  2. 데이터베이스 서버를 마이그레이션한다면, 예를 들어 PostgreSQL을 사용하던 데이터베이스를 MySQL로 옮긴다면 쿼리를 수정해야 할 필요가 있다.

ORM

ORM은 Object Relation Mapping의 약자다. ORM은 RDB의 테이블 구조객체지향 프로그래밍 방법론에서의 객체가 꽤 많이 닮아 있다는 것에서 출발한다. 테이블에서 하나의 레코드는 하나의 객체와 매핑되고, 하나의 열과 타입은 객체의 필드와 타입으로 매핑할 수 있다. 결론적으로 ORM은 쿼리와 쿼리의 결과를 각각 string과 Map으로 관리하지 않고, 메소드와 객체로 이루어진 list로 관리하자는 것이다. 이것도 코드로서 예시를 들어 보겠다.

객체지향 표현 방식 중 하나인 클래스로 스키마를 매핑하고, 특별한 메소드를 이용해 쿼리하여 객체로 이루어진 리스트를 얻어온다. 문자열 형태의 SQL을 사용할 때에 비하면, 어플리케이션과 데이터베이스 간의 이질감이 줄어들었다. 그림으로 표현하면 아래와 같다.

객체 단위로 쿼리를 생성하고, ORM 라이브러리는 이를 raw string SQL로 변환하여 실제로 쿼리를 수행한다. ORM 라이브러리는 데이터베이스를 제공하는 vendor마다 어떤 방식으로 SQL을 작성해야할 지를 알고 있기 때문에, ORM은 데이터베이스 액세스에 대한 추상화 레벨을 높일 수 있는 가장 쉬운 방법이다. 그러나 ORM은 매우 많은 논쟁을 끌고 다니는 기술이다. 이는 Should I or should I not use ORM?이나 마틴 파울러의 ORMHate, Why we don't use ORM등의 글을 읽어보면 도움이 될 것 같다.

'배경지식' 카테고리의 다른 글

ProxySQL  (1) 2019.02.12
직렬화와 JSON  (0) 2019.02.12
HTTP 메소드  (0) 2019.02.12
StatsD  (0) 2019.02.11
HTTP 헤더  (0) 2018.11.02

HTTP라는 프로토콜은 그 이름(Hypertext Transfer Protocol)에 맞게 하이퍼미디어 문서 전송을 위해 웹 브라우저와 웹 서버 사이의 통신을 위해 설계되었지만, 연결을 열고 - 요청을 보낸 뒤 - 응답을 대기하는 고전적인 클라이언트-서버 모델이 필요한 곳이라면 웹 문서 전송이 아닌 다른 목적을 위해서도 사용할 수 있다. 이런 곳에서는 백엔드 레이어를 구현할 때 메소드(method) 개념을 생각하게 된다. 클라이언트에게 동일한 URL에 대해 생성/조회/삭제 등의 동작을 제공하는 게 명료하기 때문이다. 메소드는 HTTP 요청에서 사용하며, 클라이언트가 웹서버에게 요청의 목적이나 종류를 알리는 수단으로서 사용한다. HTTP 메소드에는 종류가 참 많은데도 불구하고 나는 GET/POST/PATCH/PUT/DELETE 정도만 사용하고, 알고 있었던 것 같아 한 번 정리해보고자 한다.

HTTP 메소드

HTTP의 요청에서 URL, 헤더, body와 함께 중요한 구성 요소다. 위에서 말했듯 자원(URL)에 대한 행위(verb)를 명시하기 위해 사용된다. 조회, 전송, 삭제 등의 행위에 따라 여러 종류의 메소드가 존재한다.

GET

GET /news HTTP/1.1

리소스의 표시를 요청한다. URL에 웹 브라우저로 접속하는 것이 대표적인 GET 요청의 예다. 게시글 목록, 주소록 등을 불러오는 것처럼, 데이터를 받아오는 맥락의 HTTP 요청은 통상적으로 GET을 사용한다. 대부분의 경우 GET 요청에는 body가 없다.

POST

POST /post HTTP/1.1

리소스에 데이터를 전송한다. 새로운 게시글을 작성하거나, 회원가입을 하는 등 새로운 데이터를 서버에 등록하기 위해 일반적으로 POST를 사용한다.

PUT

PUT /meal/2018/12/25 HTTP/1.1

POST와 동일하게, 리소스에 데이터를 전송한다. '생성하되, 이미 존재한다면 변경한다'라는 논리가 깔려 있는 것이 한 가지 다른 점이다. 사용자의 설정 데이터 적용이나, 날짜별 급식 정보 업로드처럼 POST와 PATCH를 구분짓기 애매하거나 클라이언트 레벨에서 생성과 수정을 따로 구현하기 귀찮아하는 기능에서 종종 사용했던 것 같다.

PATCH

PATCH /post/9e15c13aed1c48b5 HTTP/1.1

리소스의 특정 부분을 수정하는 데에 쓰인다. 게시글 수정이나 닉네임, 자기소개 변경과 같이 update 개념의 기능에서 자주 쓰인다.

DELETE

DELETE /me/profile-picture HTTP/1.1

리소스를 삭제한다. 친구 삭제나 프로필 사진 제거와 같은 기능에서 쓰인다.

HEAD

HEAD /dumps/log/1540993830.csv HTTP/1.1

리소스를 GET 메소드로 요청하는 경우에 어떤 헤더들이 반환되는지를 요청한다. 큰 용량의 리소스를 다운로드 받을지 말지 결정하기 위해서 Content-Length를 알아내거나, 응답에 영향을 줄 수 있는 헤더 목록을 조회하기 위해 Vary 헤더를 얻어오는 용도 등으로 사용할 수 있다. HEAD 메소드의 요청에 대한 응답은 body를 가질 수 없고, 만약 가지더라도 무시된다.

OPTIONS

OPTIONS * HTTP/1.1

자원에 대한 통신 옵션을 요청하기 위해 사용된다. 웹 클라이언트의 Axios 모듈에서 Access-Control-Allow-Origin 등과 같은 CORS 정보를 얻어내는 것이 한가지 예다. 서버 전체를 나타내기 위해 asterisk(*)를 사용하기도 한다.

클라이언트 사이드 렌더링HTTP API 기반의 웹 서비스가 활발히 만들어지고 있는 요즘에는 HEAD, OPTIONS와 같은 특별한 것들을 제외한 메소드들은 어떤 용도로 사용되어야 하는지에 대한 정리가 사실상 proposal 수준으로 받아들여지고 있는 것 같다. 따라서 '조회에는 무조건 GET을 써야 한다'라기보단 '조회와 비슷한 맥락의 기능에는 GET을 쓰는 게 좋다' 정도로 생각하면 될 것 같다. 내 경우에도 '목록 조회' 동작임에도 불구하고 요청 데이터의 양이 많아서 JSON body를 사용하도록 하기 위해 POST로 API를 구성할 때가 종종 있었다. ElasticSearchDruidGET에도 body를 포함시킬 수 있는데, 이게 웬만한 웹 프레임워크들에서도 지원된다면 참 좋겠다.

'배경지식' 카테고리의 다른 글

ProxySQL  (1) 2019.02.12
직렬화와 JSON  (0) 2019.02.12
ORM  (0) 2019.02.12
StatsD  (0) 2019.02.11
HTTP 헤더  (0) 2018.11.02

StatsD는 로그 메트릭을 수집하는 영역에서 ProxySQL과 비슷한 용도로 쓰인다. UDPTCP를 통해 countertime과 같은 통계 데이터를 받아, Graphite같은 backend에 전송하는 Proxy다. 따라서 ProxySQL처럼 connection 중계가 아니라 통계적인 데이터 집계 처리 용도의 proxy로서 사용된다.

조금 더 쉽게 설명하면, StatsD는 메소드 호출 빈도와 소요 시간, 서비스 내에서 사용자가 일으키는 conversion(로그인, 장바구니 담기, 결제 등) 수 등을 MongoDB, MySQL, Datadog, zabbix, influxdb와 같은 backend에 적절히 기록한다. Graphite와 가장 케미가 좋다. (StatsD가 지원하는 backend 목록)

StatsD가 주는 메리트

ProxySQL처럼 auto failover와 같은 high availability 지원도 안 되는데 왜 굳이 쓰나? 싶겠지만, 애초에 StatsD를 비롯한 proxy 구조 자체의 이점이 있다. 바로 client-side 지원이다.

만약 InfluxDB로 로그 메트릭을 수집하고자 하더라도, 사용하는 언어에 안정적인 InfluxDB client 라이브러리가 없다면 번거로울 수밖에 없다. 이 경우 StatsD같이 InfluxDB를 backend로 지원하는 프록시 라이브러리를 사용하면 되고, 없더라도 구현하기 비교적 더 쉽다. TCP/UDP 소켓 기반으로 간결하게 통신할 수 있도록 설계되어 있기 때문이다. 그리고 StatsD는 TCP/UDP 프로토콜과 꽤 밀접하게 붙어 있기에, HAProxy같은 소프트웨어 로드 밸런서를 통해 high availability도 보장해줄 수 있다.

추가적으로 StatsD는 통계 데이터 집계 면에서 우수하다. StatsD는 데이터를 모아 두다가 일정 시간마다 집계 처리를 하고, 이들을 한번에 backend로 전송한 후 StatsD의 데이터를 모두 비운다. 이 때문에 로그를 visualize하기 이전에 통계 데이터를 미리 집계해둘 수 있다. 들어오는 데이터를 모두 실시간으로 전송하는 것보다, 잠시동안 모아 뒀다가 집계 작업 후 한번에 전송하는 게 추후의 집계 작업을 위한 컴퓨팅 비용이나 네트워크 비용 면에서 우수하다.

Metric의 형태

조금 추상적인 단어긴 한데, 어떤 데이터가 오가는 형태를 metric이라고 부를 수 있을 것 같다. StatsD도 데이터를 표현하기 위한 포맷이 존재하고, 아래와 같다.

<bucket>:<value>|<type>

예를 들면, gorets:1|c가 될 수 있다. 'gorets'로 명시된 bucket은 namespace 개념이다. 그냥 key라고 생각해도 된다. bucket은 StatsD에서 이야기하는 이름이고, metric name이라고 말할 수도 있다. 구분자(|) 뒤에 'c'로 명시된 type은 StatsD에서 제공하는 타입들 중 하나다.

StatsD는 데이터를 잠시 보관해 뒀다가 일정 시간마다 이들을 backend에 전송한다고 했다. 이와 같은 pipelining을 StatsD에선 flush interval이라 부르며, 기본값은 10초다.

Metric Type

StatsD에는 4개의 metric type(Counting, Timing, Gauges, Sets)이 있다.

Counting(:|c)

'c'로 표현한다. method_check_id_duplicated:1|c처럼 사용할 수 있다. 이는 간단한 카운터로, bucket(metric name)에 value를 더하는 작업이다. 메소드나 API 호출 수 등에 사용할 수 있다. StatsD에선 bucket마다 카운트 값을 누적하고 있다가, flush interval마다 backend에 카운트의 합을 전송하고 StatsD에선 count를 0으로 재설정한다.

Timing(:|ms)

'ms'로 표현한다. api_user_auth:82|ms처럼 사용할 수 있다. 이 경우, metric name인 api_user_auth를 위해 value인 82ms가 소요되었다는 의미이다. API별 소요 시간 등에 사용할 수 있다. StatsD에 이 데이터를 쌓으면, flush interval마다 평균, 표준 편차, 합계, 상한, 하한을 계산하여 backend에 이들을 전송한다.

Gauges(:|g)

'g'로 표현한다. goroutines_a:-8|g처럼 사용할 수 있다. 말 그대로 '게이지'를 표현하기 위해 사용하며, counting에 비해 값을 감소시킬 수 있어서 하드웨어 사용량같은 측정 값이나 스레드 갯수처럼 위아래로 이동할 수 있는 counter 등에 사용할 수 있다.

Sets(:|s)

's'로 표현한다. user:planb|s처럼 사용할 수 있다. flush interval마다 bucket에 전달된 고유한 value의 수를 계산하여 backend로 전송한다. SQL로 따지면 distinct count라고 생각하면 된다. MAU나 DAU 등을 뽑아낼 때 사용할 수 있을 것 같다.

'배경지식' 카테고리의 다른 글

ProxySQL  (1) 2019.02.12
직렬화와 JSON  (0) 2019.02.12
ORM  (0) 2019.02.12
HTTP 메소드  (0) 2019.02.12
HTTP 헤더  (0) 2018.11.02

HTTP에서 헤더는 클라이언트와 서버가 요청/응답 본문과 함께 추가 정보를 전달하기 위한 요소다. 요청과 응답에서 모두 사용하는 개념이며, 대소문자를 구분하지 않는 key와 value를 콜론(:)으로 구분하여 헤더를 구성한다. 아래는 HTTP 요청과 응답 메시지의 예인데, 잘 보면 HTTP의 헤더가 많은 부분을 차지하고 있다. 언어, 브라우저 정보, 본문의 길이, 본문의 포맷, 인코딩 정보, 서버 정보 등 주고받을만 한 메타데이터들이 많기 때문이다.

헤더는 그 context에 따라 그룹을 나눌 수 있는데, 다음과 같다.

  • 일반 헤더(General header) : 요청 및 응답에는 적용되지만, 전송되는 데이터와는 관련이 없는 헤더
  • 요청 헤더(Request header) : 가져오고자 하는 리소스, 또는 클라이언트 자체에 대한 자세한 정보가 포함된 헤더
  • 응답 헤더(Response header) : 응답에 대한 추가 정보가 포함된 헤더
  • 엔티티 헤더(Entity header) : 요청이나 응답의 본문에 대한 더 많은 정보가 포함된 헤더

이제 헤더들을 살펴보자. '이런것도 있네' 싶은 헤더들이 종종 있다. 글의 길이가 너무 길어질 것 같아 여러 편에 나누어 구성한다.

Authorization: Basic cGxhbmI6c2VjcmV0

Authorization: <type> <credentials>

Request header로, 사용자의 자격을 증명하기 위해 사용한다. 게시글을 삭제하는 작업을 처리하는 API에서 요청의 Authorization 헤더를 복호화한 뒤 요청자가 리소스에 대한 삭제 권한이 있는지를 확인하는 등의 구현이 있을 수 있다.

type과 credentials를 공백으로 구분하여 value를 구성하며, type은 인증 타입을 나타낸다. BearerBasic, Digest, OAuth 등을 값의 맨 앞에 두는 것을 자주 봤을 것이다. type에 따라 credentials를 구성하는 방식이 달라지므로 서버가 credentials를 복호화하는 방식을 결정할 수 있게 하기 위해서 함께 사용한다. 따라서 credentials는 type에 따라 만들어내는 방식이 달라진다. 예를 들어 Basic 타입은 사용자명과 비밀번호를 콜론을 통해 합친 후(planb:secret), 그 문자열을 base64로 인코딩(cGxhbmI6c2VjcmV0)하여 credentials를 생성한다.

일반적으로, Authorization 헤더가 필요한 곳에 해당 헤더 없이 접근했거나, type이 유효하지 않거나, 복호화가 불가능한 경우 401을, 복호화에 성공했으나 자격 증명에 실패한 경우 403을 상태 코드로 응답한다.

Cache-Control: no-cache

Cache-Control

X- 헤더

'배경지식' 카테고리의 다른 글

ProxySQL  (1) 2019.02.12
직렬화와 JSON  (0) 2019.02.12
ORM  (0) 2019.02.12
HTTP 메소드  (0) 2019.02.12
StatsD  (0) 2019.02.11

+ Recent posts