2015년 6월 10일 수요일

Why should I have written ZeroMQ in C, not C++에의 요약

http://www.250bpm.com/blog:4

처음 ZeroMQ개발시 C++를 선택한 이유
1. data structures and algorithms(STL)의 라이브러리가 언어에 포함되어 있음
2. OOP가 가지는 장점
3. C에서는 virtual function을 사용하기 복잡하다.
4. block의 끝에서 descructor가 자동으로 호출된다.

절대 죽지 않아야 하는 ZeroMQ에게 있어서 error handling은 매우매우 중요하다. 그런데, C++의 exception은 에러를 발생시키는 지점과 에러를 처리하는 지점이 다르게 되어 있다. 이점이 undefined behaviour를 발생시키지 않아야 하는 ZeroMQ에게 있어서 좋지 않다. C처럼 에러의 발생과 처리가 한군데에서 이루어 지는 것이 좋다.
이것때문에 C++에서 exception을 사용하지 않는 형태로 ZeroMQ를 구현하였다.

그러나 여전히 문제는 남아있다. object의 initialization과 termination시에 문제가 발생한다. constructor는 return값이 없으므로 에러를 처리하려면 exception을 발생시켜야 한다. exception을 사용하지 않기로 했으므로 이를 에러를 발생시키지 않는 constructor와 에러가 발생할 수 있는 init함수로 분리하였다. 그러다보니 state의 문제가 발생한다.(semi-initialized) 이것은 descturtor의 경우도 마찬가지다. 에러가 어디서 일어나느냐에 따라 다양한 state가 있을 수 있기 때문에 이에 대한 처리는 매우 복잡해진다. C를 사용하면 이 문제가 매우 간단해진다.

C++은 undefined behaviour 보다는 빠른 개발이 더 필요한 환경에 적합한 것으로 생각된다. 시스템 프로그래밍은 C가 여전히 최고다.

AOSA의 내용중 ZeroMQ에 대한 내용 요약

http://www.aosabook.org/en/zeromq.html

ZeroMQ는 messaging system이다. 처음에는 엄청 빠른 messaging system을 만드는 것으로 시작하였다. 그래서 처음 1년은 벤치마킹 방법을 고안하고 가능한한 효율적인 아키텍쳐를 만드는데 주력하였다. 2년째에는 distributed application을 만들고 임의의 message pattern이나 다양한 전송 방법이나 여러 language binding을 지원하는데 초점을 맞추었다. 3년째에는 사용성을 향상시키고 배우기 쉽도록 만드는데 주력하였다.

1. Application vs Library

ZeroMQ는 messaging server가 아닌 library다.
퍼포먼스의 측면에서 볼 때 중간에 서버가 있으면 네트워크를 두번 타게 되는 단점이 생기고 이 서버가 bottleneck의 원인이 될 수 있다.
두번째로 생각해 볼 수 있는 것은 large-scale deployment이다. deployment가 여러 회사에 걸쳐 있게 되면 각각의 회사들은 다른 회사에 서버가 있기를 원하지 않을 것이다. 그러다보니 각각의 회사에 서버가 있게 되고 이것은 관리라든지의 어려움이 생기게 된다. 이것을 해결하려면 각각의 컴포넌트가 별도로 동작하게 하는 완전한 분산 아키텍쳐가 필요하게 된다. 결국 messaging library이다.
ZeroMQ는 서버없이 메시징을 어떻게 할 수 있을까에서 시작하였다. 이에대한 결론이 library이다.
이 아키텍쳐가 더 효율적이고 더 유연하다는 것을 그동안 증명할 수 있었다.
의도하지 않았던 결론중의 하나는 라이브러리 모델이 제품의 사용성을 향상시켰다는 것이다. 사용자들은 서버를 설치하고 운용하지 않아도 된다는 것에 많은 점수를 주었다.

배운점: 처음 프로젝트를 시작할 때 가능하다면 라이브러리의 형태로 만드는 것을 고려해라.

2. Global State

라이브러리에 전역변수는 좋지 않다. 라이브러리는 한 프로세스내에서도 여러번 로드될 수 있는데 이렇다 하더라도 전역변수는 하나만 있게 된다. 그러면 서로 다른 두 군데서 라이브러리의 전역변수는 사용하려고 하면 race condition이나 실패같은 상황이 발생 할 수 있다.
이 무제를 해결하기 위해 ZeroMQ는 전역변수를 사용하지 않는다. 따라서, ZeroMQ를 사용하는 사용자가 필요한 전역변수를 만들어서 사용해야 한다. 이것은 context라고 부르는데 전역변수를 가지고 있는 하나의 object라고 말할 수 있다. 만약 두 군데서 라이브러리를 사용하고 각자 자신의 context를 가지고 사용한다면 서로서로 상대방의 context의 존재를 모를 것이다.

배운점: 라이브러리에서 전역변수를 사용하지 말아라.

3. Performance

메시지 시스템의 속도는 throughput과 latency로 이야기 할 수 있다: throughput - 주어진 시간에 얼마나 많은 메시지가 지나 갈 수 있는가, latency - 메시지가 도착하는데 얼마의 시간이 걸리는가.
여기서 중요하게 이해하고 있어야 하는 점이 있다. latency는 A에서 B로 도착하는 데 각각의 메시지가 걸리는 시간을 의미하는 것이고 throughput은 A에서의 throughput, B에서의 throughput으로 표현되는 것이라는 것이다.

배운점: 직면한 문제를 명확하게 이해하라.

4. Critical Path

아래의 세가지가 퍼포먼스에 중요한 역할을 한다.

- Number of memory allocations
- Number of system calls
- Concurrency model

하지만, 모든 memory allocation이나 system call이 중요하지는 않다. 메시징 시스템에서는주어진 시간동안 A에서 B로 얼마나 많은 메시지를 전달할 수 있느냐가 중요하다. 또한 한 메시지를 얼마의 시간만에 전달할 수 있느냐도 중요할 수 있다.
매우 자주 사용되는 코드의 부분을 critical path라 부르는데 이 부분이 최적화에 초점이 맞추어져야 하는 부분이다.

배운점: critical path에 있지 않은 부분을 최적화하려 하지 마라.

5. Allocating Memory

message allocation과 message copying을 잘 조절하는 것이 퍼포먼스에 중요하다. 작은 메시지는 copy를 하는 것이 낫고, 큰 메시지는 allocation을 하는 것이 낫다.
ZeroMQ는 handle이라고 하는 것을 사용하는데 작은 메시지의 경우는 handle에 데이터를 저장하고 큰 메시지의 경우는 데이터에 대한 포인터만을 저장한다.

배운점: 퍼포먼스를 생각할 때 한가지만을 생각하지 마라. 각각의 경우(작은 메시지와 큰 메시지)에 다른 해결책이 있을 수 있다.

6. Batching

4개의 메시지를 각각 따로 보낸다면 전체 네트워크 스택을 4번 타야하기 때문에 좋지 않다. 이런 경우 여러 메시지를 한번(batching)에 보낼 수 있다면 좋을 것이다(throughput의 향상).   하지만, 반대로 이것은 latency가 나쁘게 된다.
ZeroMQ는 다음의 방법을 사용한다. 메시지가 많지 않고 네트워크 스택의 bandwidth를 넘지 않으면 latency를 향상시키기 위해 batching을 사용하지 않는다. 네트워크 스택의 bandwidth를 넘으면 메시지를 큐에 저장한다. 이러한 상황에서는 batching을 사용한다.

배운점: throughput을 향상시키기 위해 제일 위의 레이어에서만(아래의 레이어에서의 batching은 의미없다) batching을 사용한다. 메시지가 쌓이는 경우만 batch를 사용한다.

7. Architecture Overview

사용자는 여러 peer와 통신할 수 있는 socket이라는 것으로 ZeroMQ와 통신한다.
socket object가 사용자의 쓰레드에 있고 이 외에 ZeroMQ는 통신의 비동기를 다루기 위해 여러개의 worker 쓰레드를 만든다.(네트워크로부터 데이터 읽기, 메시지 큐에 넣기, 접속 연결 허락하기 등)
worker 쓰레드에는 여러 object가 있는데 이들은 하나의 parent를 가진다. 이 parent는 다른 쓰레드에 있을 수 있다. 대부분의 object의 parent는 socket이다. object의 parent의 parent가 socket인 경우도 있다. 따라서 socket을 시작으로 하는 tree가 생기게 된다. object는 자신의 children이 사라져야(shut down) 자신도 사라질 수 있기 때문에 이에 tree를 이용한다.
message passing에 관련이 있는 object와 관련이 없는 object로 해서 두 종류의 비동기 object가 있다. message passing에 관련이 없는 object는 주로 connection management와 관련이 있다. 예를 들어 TCP listener object는 접속하려는 TCP 연결을 기다리고 각각의 새 연결에 engine/session object를 만든다. 비슷하게 TCP connector object는 TCP peer에의 연결을 시도하고 이것이 성공하면 연결을 관리하는 engine/session object를 만든다. 연결이 실패하면 다시 연결하기를 시도한다.
message passing에 관련이 있는 object는 data transfer를 다루는 object들이다. 이 object들은 두 부분으로 구성되어 있다. session object는 ZeroMQ socket과 통신을 하고 engine object는 네트워크 부분과 통신을 한다. session object는 한 종류만 있지만 engine object는 여러 type(TCP engines, IPC engines, PGM engines등)이 있다. 추가도 가능하게 되어 있다.
session은 socket과 message를 교환한다. message를 전달하는 것은 양방향으로 되어 있고 각각은 pipe object에 의해 다루어진다. 각각의 pipe는 쓰레드들 사이에 메시지를 빠르게 전달하는데 최적화된 lock-free 큐이다.
마지막으로, 모든 socket과 모든 비동기 object들에 의해 사용이 가능하고 global state를 저장하고 있는 context object가 있다.

8. Concurrency Model

멀티코어를 잘 이용하는 것은 ZeroMQ의 요건중의 하나다. 다시 말하면 코어가 증가하면 throughput이 증가하는 것을 의미한다.
전통적인 방법(critical sections, semaphores등)으로 여러 쓰레드를 사용하는 것은 성능을 향상시키지 않는다. 여러 쓰레드를 사용하면 코어가 여러개라도 하나의 쓰레드를 사용하는 것보다 느릴 수 있다.
ZeroMQ는 actor model이라 불리는 다른 모델을 사용한다. 쓰레드들 간의 통신에 쓰레드들 사이에 전달하는 비동기 메시지를 사용하는 것이다.
한 코어당 하나의 worker thread만을 사용한다. TCP engine 같은 각각의 내부 ZeroMQ object들은 특정한 worker thread에 연결되어 있다. 따라서 critical section이나 mutex나 semaphore같은 것을 사용할 필요가 없게 된다. 또한 이 object들은 CPU 코어 사이를 이동할 필요가 없게 되기 때문에 cache pollution을 피할 수 있게 된다.
이 방법은 기존의 멀티 쓰레딩의 문제를 사라지게 한다. 하지만 여전히 많은 object들 사이에 worker thread를 공유할 필요가 있다. 이벤트의 임의의 시퀀스를 다루어야 하고 object가 CPU를 오래 사용하면 안되도록 해야 하기 때문에 하나의 이벤트 루프를 사용하기 보다는 event-driven 방식으로 object를 관리하는 스케줄러가 필요하다는 것을 의미한다.
전체 시스템은 완전히 비동기적이어야 한다. blocking을 사용하는 object가 없어야 한다. blocking을 어떤 하나의 object가 사용하면 같은 worker thread를 공유하는 다른 쓰레드가 block될 수 있기 때문이다. 모든 object는 명백히든 내부적으로든 state machine이어야 한다. 동시에 실행중인 수백, 수천개의 state machine사이에 가능한 모든 통신을 다룰 수 있어야 한다. 특히 중요한것이 shutdown process이다.
완전히 비동기적인 시스템에서의 shutdown은 매우 복잡한 작업이다. 이 부분이 ZeroMQ에서 가장 복잡한 부분이다.버그 트래커를 보면 30-50%정도가 shutdown에 관련되어 있다.

배운점: 진짜로 performance와 scalibility를 고려한다면 actor model를 고려해보라.

9. Lock-Free Algorithms

lock-free algorithm은 커널이 제공하는 mutex나 semaphore를 사용하지 않고 쓰레드간에 통신을 하기 위한 방법이다. atomic compare-and-swap(CAS)같은 CPU의 기능을 사용해서 동기화를 한다. 이것은 하드웨어 레벨에서 lock이 이루어지기 때문에 진짜로 lock-free이지는 않다.
ZeroMQ는 pipe object에서 lock-free queue를 사용해서 user thread와 worker thread사이의 메시지를 전달한다. ZeroMQ가 lock-free queue를 사용하는 두가지 흥미로운 측면이 있다.
첫째로, 각각의 queue는 하나의 write thread와 하나의 reader thread를 가진다. 1:N 통신이 필요하면 여러 queue가 만들어진다. 이렇게 하면 writer들이나 reader들에의 동기화를 신경쓸 필요가 없어지기 때문에 매우 효율적이 된다.
둘째로, lock-free algorithm이 효율적이기는 하지만 여전히 우리가 원하는 만큼의 성능을 주지는 않았다.
이것을 향상시키기 위해 사용한 방법이 batching이다. 10개의 메시지가 있다고 하자. 이 메시지들을 하나씩 queue에 넣기 보다는 10개를 모은 후 한번에 queue에 저장하는 것이 낫다.
같은 방법을 reader쪽에도 적용할 수 있다. 메시지를 하나씩 읽기보다는 이 메시지들을 reader thread만이 읽을 수있는 queue의 특정영역(pre-read buffer)에 두고 reader thread가 한번에 읽을 수 있게 한다.

배운점: lock-free algorithm은 사용하기도 어렵고, 구현하기에도 문제 많고 디버깅하기도 좋지 않다. 가능하다면 기존의 가능한 방법을 사용하자. 너무 lock-free algorithm에 의존하지 말아라.

10. API

user interface는 제품에서 매우 중요한 부분이다. 라이브러리에서는 API가 될 것이다.
ZeroMQ의 초기 API는 exchange와 queue에 AMQP의 model을 따라 만들었다. 이것을 다시 처음부터 만들 때 BSD Socket API를 따라 만들었다. 이전에는 전문가가 사용하는 제품이었다면 이후부터는 누구든지 쉽게 사용할 수 있는 제품이 되었다. 커뮤니티는 10배로 커졌고 20개 이상의 언어의 바인딩이 만들어졌다.

배운점: 만들고자 하는 것을 알고 그에 맞는 UI를 만들어라.

BSD Socket API가 오래되었지만 많은 사람들이 알고 있고 쓰고 있는 것이기 때문에 ZeroMQ의 API를 배우기가 쉬워졌다. 또한 TCP, UDP, file, pipe같은 기존의 것과 같은 API를 사용하기 때문에 이것들과 같은 데서 사용될 수도 있고 기존의 것에 ZeroMQ를 쉽게 추가할 수도 있다. 그리고 BSD Socket API가 오래 사용되었다는 것은 이것이 좋은 디자인으로 만들어졌다고 말할 수 있는 것이다. 이것을 따른다는 것은 우리의 API도 좋은 디자인을 따라간다고 할 수 있다.

배운점: 제품을 만들 때 관련 제품을 살펴보라. 어떤 제품이 실패했고 어떤 제품이 성공했는지를 보고 배워라. 배울 수 있는 모든 것(idea, API, framework등)은 다 배워라.

11. Messaging Patterns

메시징 시스템에서 가장 중요한 문제는 사용자가 어떤 메시지를 어디로 보낼 것인지를 정하는 방법을 어떻게 사용자에게 제공할 것인가 하는 것이다. 이에는 두가지 접근법이 있다.
첫째로는 "do one thing and do it well"이라는 유닉스 철학을 따르는 것이다. 이것이 의미하는 바는 문제를 제한된 영역으로 제한하여 해결해야 하는 문제를 해결하는 것이다. 이것의 예로는 MQTT가 있다.
두번째로는 일반성에 초점을 맞추어 강력하고 구성이 가능한 시스템을 만드는 것이다. AMQP가 이것의 예이다. 이 모델은 어떤 routing algorithm도 정의할 수 있게 해주는 수단을 사용자에게 제공한다.
ZeroMQ는 첫번째 모델을 따른다. 첫번째 모델은 누구나 쉽게 사용할 수 있지만 두번째는 전문가정도는 되어야 사용이 가능한다.
특정 문제에 맞는 해결을 하는 것이 일반적인 해결을 하는 것보다 낫기는 하지만 그래도 사용자에게 다양한 기능을 제공하는 것이 좋다. 이 모순을 어떻게 해결할 수 있을까?
아래의 두단계로 해결할 수 있다.
1. 특정한 문제 영역을 다루기 위한 레이어를 정의한다.(transport, routing, presentation등)
2. 서로 관련이 없는 각 레이어의 구현을 만든다.
인터넷 스택에서 transport layer를 예로 들어보자. IP layer위에 다양한 서비스(TCP, UDP, SCTP등)가 존재한다. 이들은 서로 연관을 맺지 않는다. 따라서 쉽게 새로운 서비스를 추가할 수 있고 기존의 것을 제거할 수도 있다.
같은 원리가 messaging pattern에도 적용된다. messaging pattern은 transport layer위로 scalibility layer라 불리는 layer를 구성한다. 각각의 messaging pattern은 이 레이어의 구현이다. publish/subscribe나 request/reply는 서로 연관없이 동작한다.

배운점: 복잡하고 다양한 측면을 가지는 문제를 해결하고자 할 때 하나의 일반적인 해결책이 최선이 아닐 수 있다. 대신에 문제 영역을 잘 추상화하고 각각의 레이어에 다양한 구현을 제공한다.

12. Conclusion

이 문서는 시스템적인 측면에서 large-scale distributed system을 만들면서의 우리의 경험을 이야기한다.

Linux Performance and Tuning Guidelines 초간단 요약


- 병목점 찾기
1 시스템 파악
2 시스템 백업
3 시스템의 성능 모니터링하고 분석
4 문제가 있는 병목점을 정한 후 원인 찾기
5 한번에 하나씩 시도하며 병목점 문제 해결
6 시스템의 성능이 만족스러울 때까지 3으로 돌아가 반복

- CPU performance tuning
ps -ef로 확인하여 불필요한 프로그램이 돌고 있으면 cron을 써서 중요하지 않은 시간에 돌도록 변경
CPU를 많이 쓰나 중요하지 않은 프로세스는 priority를 조정(top, renice)
SMP 머신의 경우 프로세스가 여러 프로세서에서 돌지 않도록 해준다(taskset)
실행중인 애플리케이션에 따라 CPU를 증설하는 것보다는 더 빠른 CPU로 바꾸어 주는 것이 나을 수 있다.
최신 driver나 firmware를 사용한다.

- Memory performance tuning
bigpages, hugetlb, shared memory를 사용해서 swap space를 튜닝
page의 사이즈를 늘리거나 줄이기
active와 inactive memory를 다루는 방법을 향상시키기
page-out rate를 조절하기
서버에서 각각의 유저가 사용가능한 리소스 제한하기
불필요한 서비스 중단하기
메모리 늘리기

- Disk performance tuning
workload가 sequential 때문이라면 더 빠른 disk controller를 설치한다. workload가 random 때문이라면 drive를 추가한다.
RADI환경에서는 disk drive를 추가한다. 하드웨어 RAID를 사용한다.
striping없는 logical volume을 사용하는 것보다 striping있는 logical volume을 사용하는 것을 고려해라.
네트워크상의 다른 시스템으로 프로세싱 넘기기 - Offload processing to another system in the network (users, applications, or services).
더 많은 램을 추가하기

- Network performance tuning
네트워크 카드의 설정이 router와 switch 설정과 잘 맞는지 확인(frame size)
subnet 조작하기
더 빠른 네트워크 카드 사용하기
적절한 IPv4 TCP 커널 파라메터 튜닝하기
네트워크 카드 바꾸고 성능 측정하기
네트워크 카드를 추가하고 바인딩하기

- Changing kernel parameters
kernel parameter에 대한 정보는 /proc(or /proc/sys)에 저장된다.
sysctl 사용
  예: > sysctl /proc/sys/kernel/kernel.shmmax
  reboot후 설정 사라짐. /etc/sysctl.conf에 위 내용 넣어두면 계속 적용

- Tuning the processor subsystem
프로세스의 priority를 변경 - renice
CPU affinity
  프로세스를 특정 CPU에서만 돌도록 하기
    irq 19를 3번째 CPU에 할당
    > echo 03 > /proc/irq/19/smp_affinity
  SMT(symmetric multi-threading)시스템에서는 물리 CPU가 인터럽트를 처리하도록 하기
NUMA(Non-Uniform Memory Architecture) 시스템
  NUMA지원이 없는 애플리케이션이 병목점이 될 수 있다. numastat(numactl 패키지)로 확인

- Tuning the vm subsystem
swap 설정
  메모리 페이지 스왑하기 - /proc/sys/vm/swappiness(값을 크게하면 swap이 더 잘 일어나게 된다.)
  pdflush daemon이 얼마나 자주 flush를 하는지 설정 - /proc/sys/vm/dirty_background_ratio(값을 크게하면 flush가 더 잘 일어나게 된다.)
  애플리케이션에 의해 만들어진 dirty page를 디스크로 flush하는 비율 설정 - /proc/sys/vm/dirt_ratio(file system cache의 비율 설정)
Swap partition
  보통의 스왑 파티션은 물리 메모리의 두배
  스왑 파티션을 여러개 만드는 것이 성능을 향상시킬 수 있다.
    순차적 사용, 여러개 동시에 사용
  스왑 파티션은 가장 빠른 드라이브에 설정한다.
HugeTLBfs
  TLB(Translation Lookaside Buffer)의 크기를 늘린다. - /proc/sys/vm/nr_hugepages
    TLB - a small cache used for storing virtual-to-physical mapping information
  /proc/meminfo로 hugetlb page의 정보를 볼 수 있다.

- Tuning the disk subsystem
Hardware 고려사항
  disk I/O가 중요한 시스템: file, print, database server
  disk I/O가 중요하지 않은 시스템: email, web server
  RAID로 엮은(software and hardware RAID) drive를 통해 성능을 향상 시킬 수 있다.
  파티션 나누기
    보안을 향상시킨다.
    data integrity를 향상시킨다.
    새로운 인스톨이나 업그레이드 시 다른 파티션에 영향을 주지 않을 수 있다.
    효율적인 백업 가능
I/O elevator
  elevator 선택하기
    Synchronous file system access
      anticipatory elevator - least throughput and the highest latency
      CFQ, NOOP, deadline은 비슷하게 좋으나 I/O 사이즈가 16kb가 넘어가면 CFQ와 NOOP가 좋음
    Complex disk subsystems
      NOOP elevator
    Database systems
      seek-oriented 특성때문에 deadline elevator가 좋다.
    Virtual machines
      virtualization layer가 필요한 작업을 한다.
    CPU bound applications
      NOOP elevator
    Simple ATA or SATA disk subsystems
      anticipatory elevator
  nr_requests
    > echo 64 > /sys/block/sdb/queue/nr_requests
    disk subsystem과 I/O 특성에 따라 다르게 해줄 필요가 있다.
      nr_requests의 값 별로 I/O 사이즈에 따른 throughput 측정
  read_ahead_kb
    > echo 64 > /sys/block/<disk_subsystem>/queue/read_ahead_kb
    large streaming read의 경우 read ahead buffer의 크기를 증가시키면 성능 향상이 올 수 있다.
    random I/O operation의 경우는 별로 효과가 없을 것이다.
  file system 선택하기
    적은 I/O request의 경우 ReiserFS가 적합하고 매우 큰 파일 시스템과 매우 큰 I/O 사이즈에서는 XFS와 JFS가 좋다. Ext3는 좋은 multiprocessor scalability를 제공하면서 적은 I/O request에 적합하기 때문에 이들의 중간점이라 볼 수 있다.
    JFS, XFS: high-end data warehouses, scientific workloads, large SMP servers, or streaming media servers
    ReiserFS, Ext3: file, web, mail
    Ext2는 synchronous file system acccess시 좋음. data integrity보다 성능이 우선시 되는 경우 선택한다. asynchronous file system의 경우는 ReiserFS가 Ext3보다 좋다.
    I/O priority
      CFQ I/O elevator에서 제공하는 process에 priority를 할당하는 기능
      ionice를 사용
        idle, best-effort, real time
    Access time updates
      linux file system이 저장하는 파일의 시간정보를 저장하지 않음으로서 성능향상을 줄 수 있다.
    Journaling mode 선택하기
      mode 변경방법
        > mount -o data=writeback /dev/sdb1 /mnt/mountpoint
        또는 /etc/fstab에서 변경가능
          /dev/sdb1 /testfs ext3 defaults,data=writeback 0 0
      data=journal
        file data와 metadata를 저널링한다.
      data=ordered(default)
        metadata만 저널링한다.
      data=writeback
        특별히 적은 I/O사이즈의 경우 Ext3의 성능을 향상시킨다.
        I/O 사이즈가 증가하면 writeback의 효과는 줄어든다.
        write의 경우 성능향상이 있고 read의 경우 별 효과가 없다.
    Block sizes
      작은 파일을 많이 다룬다면 작은 block size가 좋다.
      많은 파일을 많이 다룬다면 큰 block size가 좋다.
      하지만 여러 벤치마크를 보면 block size의 변경으로 실제 성능향상을 보기는 쉽지 않다. 그러므로 디폴트인 4K로 두는 것이 일반적으로 좋다.
      hardware RAID가 사용되는 경우 array의 stripe size가 성능에 매우 큰 영향을 미친다.
- Tuning the network subsystem
네트워크 트래픽
  netstat, tcpdump, ethereal을 사용하여 정보 획득
  고려사항:
    Transaction throughput requirements (peak, average)
    Data transfer throughput requirements (peak, average)
    Latency requirements
    Transfer data size
    Proportion of send and receive
    Frequency of connection establishment and close or number of concurrent connections
    Protocol (TCP, UDP, and application protocol such as HTTP, SMTP, LDAP, and so on)
speed and duplexing
  NIC의 속도 확인
  네트워크 컴포넌트(switch or hub)와 NIC의 관계 확인
MTU size
  Maximum Transmission Units
  > ifconfig eth0 mtu 9000 up
  모든 네트워크가 큰 MTU를 지원하지는 않는다.
Network buffer 증가시키기
  memory resources
    초기 TCP memory
      /proc/sys/net/ipv4/tcp_mem
    receive socket memory
      /proc/sys/net/core/rmem_default
      /proc/sys/net/core/rmem_max
    send socket memory
      /proc/sys/net/core/wmem_default
      /proc/sys/net/core/wmem_max
    option memory buffer
      /proc/sys/net/core/optmem_max
  window size 조정
    BDP(bandwidth delay product)로 최적의 window size 값을 알아올 수 있다.
      BDP = Bandwidth(bytes/sec) * Delay(or round trip time)(sec)
    tcpdump로 변경 내용 확인
TCP/IP tuning
  IP와 ICMP tuning
    spoofing attack 막기
      > sysctl -w net.ipv4.conf.eth0.accept_source_route=0
      > sysctl -w net.ipv4.conf.lo.accept_source_route=0
      > sysctl -w net.ipv4.conf.default.accept_source_route=0
      > sysctl -w net.ipv4.conf.all.accept_source_route=0
    지정된 머신이 아닌 곳에서의 redirect 막기
      > sysctl -w net.ipv4.conf.eth0.secure_redirects=1
      > sysctl -w net.ipv4.conf.lo.secure_redirects=1
      > sysctl -w net.ipv4.conf.default.secure_redirects=1
      > sysctl -w net.ipv4.conf.all.secure_redirects=1
    ICMP redirect 막기
      > sysctl -w net.ipv4.conf.eth0.accept_redirects=0
      > sysctl -w net.ipv4.conf.lo.accept_redirects=0
      > sysctl -w net.ipv4.conf.default.accept_redirects=0
      > sysctl -w net.ipv4.conf.all.accept_redirects=0
    router가 아닌 경우 redirect 보내지 않기
      > sysctl -w net.ipv4.conf.eth0.send_redirects=0
      > sysctl -w net.ipv4.conf.lo.send_redirects=0
      > sysctl -w net.ipv4.conf.default.send_redirects=0
      > sysctl -w net.ipv4.conf.all.send_redirects=0
    broadcast ping과 smurf 공격 무시하기
      > sysctl -w net.ipv4.icmp_echo_ignore_broadcasts=1
    icmp 패킷이나 ping 무시하기
      > sysctl -w net.ipv4.cimp_echo_ignore_all=1
    router에 의한 invalid response 무시하기
      > sysctl -w net.ipv4.icmp_ignore_bogus_error_responses=1
    IP fragment를 합치는데 사용되는 메모리 설정
      > sysctl -w net.ipv4.ipfrag_low_thresh=262144
      > sysctl -w net.ipv4.ipfrag_high_thresh=393216
  TCP tuning
    TIME_WAIT socket 다시 사용하기
      > sysctl -w net.ipv4.tcp_tw_reuse=1
      > sysctl -w net.ipv4.tcp_tw_recycle=1
    tcp_fin_timeout 값 줄이기
      FIN-WAIT-2에서 소켓을 가지고 있는 시간
      > sysctl -w net.ipv4.tcp_fin_timeout=30
    동작중이지 않은 소켓 연결 끊는 시간 줄이기
      > sysctl -w net.ipv4.tcp_keepalive_time=1800
    backlog connections queue값 올리기
      > sysctl -w net.ipv4.tcp_max_syn_backlog=4096
    TCP SYN cookies는 필요할 때만 키기
      > sysctl -w net.ipv4.tcp_syncookies=1
      커널이 CONFIG_SYNCOOKIES로 컴파일되어야 함
      Dos와 DDos를 막아주기는 하지만 성능에 영향을 미침
  TCP options tuning
    selective acknowledgment 막기
      > sysctl -w net.ipv4.tcp_sack=0
      > sysctl -w net.ipv4.tcp_dsack=0
    TCP timestamp 막기
      > sysctl -w net.ipv4.tcp_timestamps=0
      backend system의 경우 굳이 time stamp 필요없음
    window scaling 막기
      > sysctl -w net.ipv4.tcp_window_scaling=0
      high network load에서는 window scaling이 좋지 않음
Netfilter
  packet filtering capability와 network security를 향상시키지만 예측가능하지 않은 행동을 유발할 수 있다.
  영향을 끼치는 요인
    Number of rules
    Order of rules
    Complexity of rules
    Connection tracking level (depends on protocols)
    Netfilter kernel parameter configuration
Offload configuration
  ethtool 사용
    > ethtool -k eth0
packet queue의 크기 늘리기
  /proc/sys/net/core/netdev_max_backlog
transmit queue length 늘리기
  > ifconfig eth1 txqueuelen 2000
  large, homogeneous data transfer를 수행하는 high-speed 연결에 적합
인터럽트 줄이기
  network interface의 인터럽트를 물리 CPU에 할당하면 성능 향상을 얻을 수 있다.
  interrupt number 알아내기
    > ifconfig eth1
  인터럽트의 CPU affinity 설정하기
    > echo 02 > /proc/irq/169/smp_affinity

- sysctl tutorial

http://www.frozentux.net/ipsysctl-tutorial/ipsysctl-tutorial.html

- 참고

https://access.redhat.com/knowledge/docs/en-US/Red_Hat_Enterprise_Linux/6/html/Performance_Tuning_Guide/index.html

Flask - Quickstart 요약

- 가장 간단한 웹앱을 만들어 보자.

# hello.py

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello World!'

if __name__ == '__main__':
    app.run()


위와 같이 입력하고 command line에서 아래와 같이 입력한다.
> python hello.py

브라우저에서 http://127.0.0.1:5000에 접속하면 'Hello World!'를 볼 수 있다.

접속 URL을 컴퓨터의 ip로 바꾸고 싶으면 app.run부분을 아래와 같이 바꾸면 된다.

=> app.run(host='0.0.0.0')

디버그 모드를 키고 싶으면 아래와 같이 한다.

=> app.run(debug=True)

'/'의 요청시 hello_world() 함수가 호출되는 것은 @app.route에 의해 이루어진다.

위에서처럼 고정된 URL이 아닌 필요에 따라 변하는 URL의 경우 매칭시키고 싶으면 아래와 같이 하면 URL에서 <username> 부분이 함수의 username 파라메터로 넘어온다.

@app.route('/<username>')
def show_user_profile(username):
    return 'User %s' % username

들어오는 값을 아래와 같이 변환해 줄 수도 있다.

@app.route('/<int:post_id>')
def show_post(post_id):
    return 'Post %d' % post_id

변환해주는 값에는 아래의 3가지가 있다.
int : 정수로 변환
float: 실수로 변환
path: 마지막 '/'도 포함하는 path

- routing rule

* 마지막에 '/'가 있는 경우:
예) '/projects/'
'/projects'로 요청하면 flask는 '/projects/'로 redirect한다.

* 마지막에 '/'가 없는 경우:
예) '/projects'
'/projects/'로 요청하면 없는 페이지로 에러 발생

지금까지는 url을 함수로 매핑시켜주는 @app.route를 살펴봤다. 반대도 가능할까? url_for()를 사용하면 된다. 첫번째 파라메터는 함수의 이름이고 두번째부터는 URL에서의 변경부분이다. 없는 두번째 파라메터는 query parameter가 된다.

url_for('hello_world') => /
url_for('username', username='soo') => /username/soo
url_for('username', query='soo') => /username?query=soo

- HTTP method에 대한 처리

@app.route('/', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        do_post_job()
    else:
        do_get_job()

디폴트는 GET이다.

- static 파일은 패키지에 static 디렉토리를 만들어서 거기에 넣어두면 /static으로 접근가능하다.

* static/style.css라는 파일이 있으면 아래와 같이 접근 가능

url_for('static', filename='style.css')

- flask는 Jinja2 템플릿 엔진을 사용한다. 물론 원하면 다른 엔진을 사용해도 된다.

패키지 안에 templates 디렉토리를 만들고 html 파일을 넣어둔다.
코드에서는 아래와 같이 접근 가능하다.

templates/hello.html 이 있는 경우

@app.route('/hello/')
@app.route('/hello/<name>')
def hello(name=None):
    return render_template('hello.html', name=name)
hello.html은 아래와 같을 것이다.

<!doctype html>
<title>Hello from Flask</title>
{% if name %}
  <h1>Hello {{ name }}!</h1>
{% else %}
  <h1>Hello World!</h1>
{% endif %}

html에서의 name은 render_template에 name argument로 넘겨준 값이 사용된다.

- 클라이언트가 보낸 데이타에 대한 처리를 위해 flask는 request object를 사용한다.

- URL에서 query parameter(?key=value)에 대한 처리는 아래와 같이 한다.

request.args.get('key', '')

- 파일 업로드하기
HTML form에서 enctype="multipart/form-data"로 해주어야 함

from flask import request

@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        f = request.files['the_file']
        f.save('/var/www/uploads/uploaded_file.txt')
    ...

업로드된 파일은 메모리나 임시 디렉토리에 저장되므로 save()함수를 사용하여 원하는 위치에 저장한다.
클라이언트쪽에서 사용했던 파일 이름을 알고 싶으면 f.filename을 사용하면 된다. 또는 좀 더 확실하게 알고 싶으면 secure_filename(f.filename)을 사용한다.

- Cookies

* 쿠키 읽기
request.cookies를 사용

from flask import request

@app.route('/')
def index():
    username = request.cookies.get('username')
    # use cookies.get(key) instead of cookies[key] to not get a
    # KeyError if the cookie is missing.

* 쿠키 저장하기
response object의 set_cookie()함수를 사용

from flask import make_response

@app.route('/')
def index():
    resp = make_response(render_template(...))
    resp.set_cookie('username', 'sun')
    return resp

- redirect하고 싶으면 redirect()함수를, 바로 종료하고 싶으면 abort()함수를 사용한다.

from flask import abort, redirect, url_for

@app.route('/')
def index():
    return redirect(url_for('login'))

@app.route('/login')
def login():
    abort(401)
    this_is_never_executed()

- 에러 페이지를 커스터마이즈하고 싶으면 app.errorhandler를 사용한다.

from flask import render_template

@app.errorhandler(404)
def page_not_found(error):
    return render_template('page_not_found.html'), 404

- Response object
a. 정상적인 response object가 함수로부터 리턴하면 이것이 바로 넘어간다.
b. string이 함수로부터 리턴하면 response object를 만들고 이것의 data로 string을 넘긴다.
c. tuple을 넘길 수 있는데 다음과 같은 형태가 되어야 한다. (response, status, headers)
d. 앞의 세 종류가 아니면 리턴 값을 response object로 변환한다.

- Session
session object를 통해 값을 가져오고 저장하고를 할 수 있다. session에는 아래의 예에서 처럼 secret key가 필요하다.

from flask import Flask, session, redirect, url_for, escape, request

app = Flask(__name__)

@app.route('/')
def index():
    if 'username' in session:
        return 'Logged in as %s' % escape(session['username'])
    return 'You are not logged in'

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        session['username'] = request.form['username']
        return redirect(url_for('index'))
    return '''
        <form action="" method="post">
            <p><input type=text name=username>
            <p><input type=submit value=Login>
        </form>
    '''

@app.route('/logout')
def logout():
    # remove the username from the session if it's there
    session.pop('username', None)
    return redirect(url_for('index'))

# set the secret key.  keep this really secret:
app.secret_key = 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT'

- 로그가 필요하면 app.logger를 사용한다.

app.logger.debug('A value for debugging')
app.logger.warning('A warning occurred (%d apples)', 42)
app.logger.error('An error occurred')

- 구글 앱 엔진에 flask를 사용한 앱을 올리고 싶으면 아래의 내용 참조하라.
https://github.com/kamalgill/flask-appengine-template

The Little Redis Book 요약

Chapter 1 - The Basics

a. Databases

Redis도 database의 개념을 가지고 있다. 처음에는 database 0로 시작한다. 이 database를 다른 것으로 바꾸고 싶으면 select 명령어를 사용한다. select 1을 입력하면 1번 database로 옮겨갈 것이다. 돌아가고 싶으면 select 0를 입력하면 된다.

b. Commands, Keys and Values

Redis를 잘 사용하려면 key와 value의 개념을 파악하고 있는 것이 중요하다. key는 data를 구별해주는 것이고 value는 key와 관련있는 실제 data이다. Redis는 value를 byte array로 다루기 때문에 value가 어떤 값이던지 상관하지 않는다.

아래의 명령어를 입력해보자.

> set users:leto "{name: leto, planet: dune, likes: [spice]}"

user:leto를 key로, 2번째 파라메터를 value로 저장하라는 명령어이다. key에서의 ':'는 Redis의 입장에서는 아무런 의미도 없다. 우리가 봤을 때 leto라는 이름의 user라는 것을 짐작할 수 있게 해주는 key일 뿐이다. value를 가져오는 명령어는 아래와 같다.

> get user:leto

c. Querying

key로 value를 찾을 수 있지만 value를 가지고 무언가를 할 수는 없다. value를 가지고 문언가를 하는 것(예: dune이라는 planet에 살고 있는 유저 찾기)은 Redis의 용도가 아니다.

d. Memory and Persistence

Redis는 in-memory persistent store이다. 기본적으로 Redis는 적당한 수 이상으로 key가 변할 때(예: key가 9번보다 적게 변하면 15분마다 disk에 저장하기) disk에 database를 복제한다. 또는 append mode로 동작하여 key가 변할 때마다 append-only file에 업데이트를 할 수도 있다.

e. Putting It Together

query limitation과 data structure와 memory에 data 저장하기 때문에 Redis는 무척이나 빠르다. 따라서 DB를 쓸 때 db에 접속량을 줄이기 위해 고민하는 것 같은 것을 할 필요가 없다.
Redis는 만능이 아니다. 필요한 용도에 적합하게 사용해야 한다.

Chapter 2 - The Data Structures

a. Strings

위에서 String을 저장하는 set를 살펴봤다. 다른 명령어들도 있다.

> strlen users:leto
> getrange users:leto 27 40
> append users:leto " OVER 9000!!"

strlen은 key의 value의 길이(length)를 가져오고 getrange는 value의 지정한 영역(range)을 가져온다. append는 기존의 value에 새 value를 더한다. 물론 지금 설명한 명령어들은 string의 경우에 의미가 있을 것이다.

> incr stats:page:about
> incr stats:page:about
> incrby ratings:video:12333 5
> incrby ratings:video:12333 3

incr은 value의 값을 1 증가시키고 decr은 1 감소시킨다. incrby는 지정한 수만큼 증가시키고 decrby는 지정한 수만큼 감소시킨다. users:leto에 incr을 적용하면 에러가 발생할 것이다. 각자 setbit과 getbit도 한번 살펴보자.

b. Hashes

key와 value 사이에 field라는 것을 두어 field를 가지고 value를 처리한다.

> hset users:goku powerlevel 9000
> hget users:goku powerlevel

hset은 key로 field와 value를 설정한다. hget으로 key에서의 field의 value를 얻어온다.

>hmset users:goku race saiyan age 737
>hmget users:goku race powerlevel
>hgetall users:goku
>hkeys users:goku
>hdel users:goku age

hmset은 한번에 여러 field를 설정할 수 있도록 해주고 hmget은 한번에 여러 field를 가져올 수 있게 해준다. hgetall은 한번에 모든 field와 이의 value를 가져오고, hkeys는 모든 field를 list로 보여준다. hdel은 지정한 field를 지운다.

이처럼 hash를 통해 좀 더 세밀한 처리를 할 수 있게 된다.

c. Lists

List는 주어진 key로 value의 배열을 처리할 수 있게 해준다.

> lpush newusers goku
>ltrim newusers 0 50

lpush는 value를 list에 더하고 ltrim은 range 사이의 value만을 유지하게 한다. ltrim은 O(N) operation을 가지는 명령어(여기서 N은 제거하는 value의 수이다.)이기 때문에 위의 두 명령어를 항상 같이 실행 함으로서 O(1)로 유지할 수 있게 된다.

d. Sets

중복되지 않는 value를 저장하고 이와 관련한 명령어들을 제공한다.

> sadd friends:leto ghanima paul chani jessica
> sadd friends:duncan paul jessica alia
> sismember friends:leto jessica
> sismember friends:leto vladimir

value가 key의 멤버인지 아닌지 알려주는 sismember는 O(1) operation이므로 매우 효율적이다.

> sinter friends:leto friends:duncan
> sinterstore friends:leto_duncan friends:leto friends:duncan

sinter는 두 key가 같이 가지고 있는 value를 알려준다. sinterstore는 sinter의 결과를 바로 새 key에 저장한다.
중복되는 value가 없는 집합에서 value의 특정한 집합을 찾거나 지정하는데 Set은 유용하다.

e. Sorted Sets

score를 가지는 set이라 할 수 있다.

> zadd friends:duncan 70 ghanima 95 paul 95 chani 75 jessica 1 vladimir

score가 90 이상인 친구를 찾고 싶으면 아래와 같이 한다.

> zcount friends:duncan 90 100

chani의 rank를 알고 싶으면 아래와 같이 한다.

> zrevrank friends:duncan chani


Chapter 3 - Leveraging Data Structures

a. Big O Notation

다수의 Redis 명령어는 O(1)이다. zadd는 O(log(N))이다. ltrim은 O(N)이지만 N이 전체 item의 수가 아니라 제거할 아이템의 수이므로 앞에서 설명했듯이 사용하면 O(1)이 된다. zremrangebyscore는 O(log(N) + M)이다.(N은 set할 element의 수이고 M은 제거할 element의 수이다.) sort는 O(N + M*log(M))이다. 이러한 내용을 인지하고 가능한 한 느린 명령어를 사용하지 않는 방향으로 해야 한다.

b. Pseudo Multi Key Queries

email과 id로 유저의 정보를 찾기 위해 아래처럼 저장하는 것은 좋지 않다.

> set users:leto@dune.gov "{id: 9001, email: 'leto@dune.gov', ... }"
> set users:9001 "{id: 9001, email: 'leto@dune.gov', ... }"

hash를 사용하면 중복을 제거할 수 있다.

> set users:9001 "{id: 9001, email: 'leto@dune.gov', ... }"
> hset user:lookup:email leto@dune.gov 9001

c. References and Indexes

앞에서 한 value가 다른 value를 reference하는 것을 봤다. 만약에 reference하고 있는 것이 바뀌면 어떻하지? Redis는 이에 대한 간편한 해결책이 없다. 수동으로 관리를 해야한다.

> sadd friends:leto ghanima paul chani jessica
> sadd friends_of:chani leto paul

d. Round Trips and Pipelining

Redis는 mget이나 sadd 같은 여러 value를 한번에 전달하는 명령어를 통해 round trip를 줄일 수 있게 해준다.
보통은 클라이언트가 한 명령을 Redis에 보내고 나면 reply를 기다린다. 하지만, Redis는 여러 명령을 먼저 보내고 나중에 response를 받는 pipelining을 지원한다. Ruby같은 경우 pipelined 함수를 통해 pipelining을 할 수 있다.

e. Transactions

여러 명령어를 atomic하게 동작시키고 싶을 때가 있다. 아래와 같이 한다.

> multi
> hincrby groups:1percent balance -9000000000
> hincrby groups:99percent balance 9000000000
> exec

지정한 key를 watch하고 있다가 그 key가 변하면 transaction을 할 수 있다. 그냥 multi와 exec만을 쓰면 모든 명령어가 한번에 실행되 버리기 때문에 이와 같은 상황에 쓸 수 없다.

redis.watch('powerlevel')
current = redis.get('powerlevel')
redis.multi()
redis.set('powerlevel', current + 1)
redis.exec()

f. Keys Anti-Pattern

keys 명령어는 pattern을 인식한다. 하지만, 이 명령어는 매우 느리다.

> keys bug:1233:*

따라서 위처럼 1233에 관련된 value를 찾고 싶은 경우 위처럼 해서 모든 key를 뒤지게 하는 것보다는 아예 처음에 저장할 때 아래처럼 하는 것이 좋은 선택이다.

> hset bugs:1233 1 "{id:1, account:1233, subject: '...'}"
> hset bugs:1233 2 "{id:1, account:1233, subject: '...'}"

Chapter 4 - Beyond The Data Structures

a. Expiration

key에 expiration을 지정할 수 있다. expire할 앞으로의 시간(second)이나 날짜(timestamp since Jan 1, 1970)를 준다.

> expire pages:about 30
> expire pages:about 1356933600

첫번째는 30초 후 key를 지우고 두번째는 2012년 12월 31일 아침 12시에 key를 지운다. ttl은 남아있는 시간을 알려주고 persist는 expiration을 제거한다. setex는 value를 set하면서 바로 expiration time도 지정한다.

> setex pages:about 30 '<h1>about us</h1>'

b. Publication and Subscriptions

> subscribe warnings
> publish warnings "it's over 9000!"

subscribe를 통해 channel에 가입하고 publish를 통해 channel에 메시지를 보낼 수 있다.

c. Monitor and Slow Log

monitor 명령어를 통해 Redis에 전달되는 모든 명령어를 모니터할 수 있다. slowlog 명령어를 통해 지정한 시간 이상이 걸리는 명령어를 log할 수 있다.
모든 명령어를 log하려면 아래와 같이 한다.

> config set slowlog-log-slower-than 0

아래의 모든 log를 보고 최근 10개의 log만 보고 log의 개수를 보는 명령이다.

> slowlog get
> slowlog get 10
> slowlog len

d. Sort

list, set or sorted set을 sort한다.

> rpush users:leto:guesses 5 9 10 2 4 10 19 2
> sort users:leto:guesses

> sadd friends:ghanima leto paul chani jessica alia duncan
> sort friends:ghanima limit 0 3 desc alpha

위의 명령은 descending order로 lexicographically 소트한 후 지정한 위치의 element를 리턴하는 명령이다.

> sadd watch:leto 12339 1382 338 9338
> set severity:12339 3
> set severity:1382 2
> set severity:338 5
> set severity:9338 4
> sort watch:leto by severity:* desc

위의 명령에서 *은 pattern에 의해 watch:leto의 value로 치환 되고 이 친환된 key의 value에 따라 sort가 된다.

Chapter 5 - Lua Scripting

a. Why?

전형적인 stored procedure를 쓰는 것이나 Lua script를 쓰는 것이나 편하지 않은 것은 똑같다. 하지만 Lua script를 잘 쓰면 성능을 향상 시킬 수 있고 코드도 간단해진다.

b. Eval

eval 명령어는 Lua script를 string으로 하는 파라메터를 받는다.

c. Script Management

eval로 실행되는 script를 매번 보내는 것은 좋은 선택이 아니다. Redis는 script를 cache하는 script load 명령을 제공한다.

redis = Redis.new
script_key = redis.script(:load, "THE_SCRIPT") // script의 sha1 digest를 리턴
redis.evalsha(script_key, ['friends:leto'], ['m'])

아래와 같은 명령어도 있다.

script kill - 실행중인 script 중단시키기
script flush - cache되어 있는 모든 script 제거하기
script exists - script가 cache에 이미 있는지 확인하기

d. Libraries

다양한 유용한 라이브러리들이 있다. table.lib, string.lib, math.ib, cjson.lib등이 있다.

e. Atomic

Redis는 single-threaded이므로 Lua script는 중간에 인터럽트 되지 않는다.

f. Administration

lua-time-limit 명령으로 Lua script가 얼마나 오래 실행될 수 있는지를 지정할 수 있다. 기본은 5초이고 보통의 Lua script는 milisecond 단위로 실행되기 때문에 이것은 꽤 긴 시간이다.

Chapter 6 - Administration

a. Configuration

redis.conf 파일을 통해 설정을 지정할 수 있다.

> config get *log*

위의 명령으로 모든 log가 들어가는 명령을 확인 할 수 있다.

b. Authentication

redis.conf 파일에 requirepass 명령으로 password를 지정할 수 있다. 이 값이 지정되면 클라이언트는 auth password 명령으로 인증해야 한다.
심각한 위험을 초래할 수 있는 명령어는 rename 할 수도 있다.

> rename-command CONFIG 5ec4db16...
> rename-command FLUSHALL 10412850...

c. Size Limitations

Redis는 수십억개의 data를 저장할 수 있다.

d. Replication

master에 data를 저장하면 하나 이상의 slave에 master의 내용을 up-to-date 한다. slave로 띄우려면 configuration 파일에 slaveof를 지정하거나 slaveof 명령을 사용한다.
Redis는 automated failover를 지원하지 않는다.

e. Backups

기본적으로 Redis는 dump.rdb라는 파일에 snapshot을 저장한다. snapshot을 뜨거나 append-only file에 저장하는 것을 하지 않고 slave에 저장하는 것을 선택할 수도 있다. 이것은 master에 부하를 줄여주는 장점이 있다.

f. Scaling and Redis Cluster

몇몇의 명령어들은 부하가 심할 수 있으므로 slave를 통한 replication은 부하를 줄여 줄 수 있다. key들을 여러 Redis instance에 분산 배치하는 것도 좋은 방법이다(consistent-hashing algorithm).
Redis Cluster는 현재 작업중에 있다.

Redis command 분석 - 2

* INCR key
Time complexity O(1)
key에 저장된 value의 값을 1 증가시킨다.

- incrCommand in t_string.c
a. incrDecrCommand(c, 1);
b. 정수값의 경우 shared.integers[value]로 미리 만들어 놓은 object를 value로 사용

* DECR key
Time complexity O(1)
key에 저장된 value의 값을 1 감소시킨다.

- decrCommand in t_string.c
a. incrDecrCommand(c, -1);

* MGET key [key ...]
Time complexity O(N)
key에 맞는 값을 찾아서 리턴. 없으면 nil을 리턴. 다수의 key를 설정하므로 multi bulk리턴

* RPUSH key value [value ...]
Time complexity O(1)
기존에 저장된 value의 끝(tail)에 value를 추가. 기존에 저장된 value가 list가 아니면 에러 리턴. key가 없으면 ziplist를 새로 만듦.
ZIPLIST와 LINKEDLIST에 사용 가능


* LPUSH key value [value ...]
Time complexity O(1)
기존에 저장된 value의 처음(head)에 value를 추가. 기존에 저장된 value가 list가 아니면 에러 리턴. key가 없으면 ziplist를 새로 만듦.
ZIPLIST와 LINKEDLIST에 사용 가능


* RPUSHX key value
Time complexity O(1)
key가 있고 list인 경우에만 끝에 value를 추가.


* LPUSHX key value
Time complexity O(1)
key가 있고 list인 경우에만 처음에 value를 추가.



* LINSERT key BEFORE|AFTER pivot value
Time complexity O(N)
value를 pivot의 이전(BEFORE)이나 뒤(AFTER)에 추가.
pivot의 encoding값이 REDIS_ENCODING_RAW이어야 함. 기존에 저장된 value들이 ZIPLIST이고 새로 저장할 value의 크기가 list_max_ziplist_value를 넘으면 LINKEDLIST로 컨버전. pivot은 앞에서부터 뒤로 loop를 돌며 찾는다.
저장한 후 value가 ZIPLIST이고 리스트의 크기가 list_max_ziplist_entries보다 크면 LINKEDLIST로 컨버전한다.

* RPOP key
Time complexity O(1)
list에서 마지막 값을 제거한다.


* LPOP key
Time complexity O(1)

list에서 처음 값을 제거한다.

* BRPOP key [key ...] timeout
Time complexity O(1)
처음으로 비어있지 않은 list의 마지막 값을 제거한다.
주어진 모든 key가 비어 있으면 timeout만큼 block된다. 하지만, MULTI/EXEC안에서 이 명령어가 실행된 것이면 바로 "*-1\r\n"을 리턴한다.
block은 리턴을 하지 않아 버리는 것이다. 그럼으로서 client는 무한정 기다리게 된다. 서버는 serverCron에서 확인하여 지정한 timeout이 지나고 나면 리턴을 한다.

* BRPOPLPUSH source destination timeout
Time complexity O(1)



* BLPOP key [key ...] timeout
Time complexity O(1)

처음으로 비어있지 않은 list의 처음 값을 제거한다.
주어진 모든 key가 비어 있으면 timeout만큼 block된다. 하지만, MULTI/EXEC안에서 이 명령어가 실행된 것이면 바로 "*-1\r\n"을 리턴한다.

Redis command 분석 - 1

* GET key
Time complexity: O(1)
key의 값(value) 얻어오기. key가 없으면 nil을 리턴
GET은 string만을 다루기 때문에 key의 값이 string이 아니면 에러가 리턴됨

- getCommand in t_string.c
a. db로부터 key를 찾는다.
b. key가 있으면 access time을 업데이트
c. key가 없으면 "$-1\r\n"을 client에 보낸다.
d. 찾은 key의 type이 string이 아니면 "-ERR Operation against a key holding the wrong kind of value\r\n"을 client에 보낸다.
e. 리턴할 value의 크기를 prefix로 해주고(예: "$2344\r\n") 뒤에 value를 붙인 후 client에게 보낸다.(예: value가 Hello인 경우 - "$5\r\nHello\r\n")

- value의 크기가 32보다 작은 경우는 자주 사용되므로 미리 shared.bulkhdr[]에 보낼 값을 만들어 놓고 사용함
shared.bulkhdr[j] = createObject(REDIS_STRING, sdscatprintf(sdsempty(), "$%d\r\n", j));

* SET key value
Time complexity: O(1)
string인 value를 key로 하여 저장. key가 이미 있으면 그 값의 타입이 무었인지에 상관없이 덮어쓴다.

- setCommand in t_string.c
a. 공간 절약을 위해 string이 숫자이면 숫자로 변환한다.
b. db에 key/value를 무조건 설정한다.(없으면 add, 있으면 overwrite)
c. expire가 설정되어 있으면 delete
d. 이 key를 watch하고 있는 client가 있으면 이 client의 flags값에 REDIS_DIRTY_CAS를 더함으로서 그 client에서의 exec가 실패하도록 해준다.
e. 항상 "+OK\r\n"을 client에게 보낸다.(set은 실패하지 않음)

* SETNX key value
Time complexity: O(1)
key가 없을 경우만 key/value를 저장한다.

- setnxCommand in t_string.c
a. key가 없을 경우는 SET과 같다.
b. key가 있을 경우 ":0\\r\n"을 key가 없을 경우는 ":1\r\n"을 client에게 보낸다.

* SETEX key seconds value
Time complexity: O(1)
주어진 시간에 timeout하는 key/value 저장
아래의 두 명령을 MULTI/EXEC block 안에서 실행하는 것과 같은 명령
    SET key value
    EXPIRE key seconds

- setexCommand in t_string.c
위의 SET 명령어에 더하여 expire에 관련된 부분만 추가해줌
a. seconds를 milliseconds로 바꾼다.
b. db의 expires로부터 key를 찾는다.(없으면 추가)
c. key의 value를 milliseconds로 바꾸어준다.

* PSETEX key milliseconds value 
Time complexity: O(1)
expire time을 millisecond로 설정한다는 것만 제외하고는 setex와 같은 명령어

* APPEND key value
Time complexity: O(1)
key가 이미 있고 string이라면 string의 끝에 value를 더한다. 없으면 key를 만들어서 value를 넣는다.

- appendCommand in t_string.c
a. key를 찾는다.
b. 없으면 key를 만들어 value를 저장한다.
c. key가 있으면 type을 확인하여 type이 string이 아니면 "-ERR Operation against a key holding the wrong kind of value\r\n"을 client에게 보낸다.
d. append할 경우 string의 size가 512MB를 넘으면 "string exceeds maximum allowed size (512MB)"를 client에게 보낸다.
e. object가 shared이거나 encoded이면 value를 copy한 후 key의 value에 덮어쓴다.
f. key의 value가 바뀌었으므로 watch하고 있는 client에 signal을 날린다.

* STRLEN key
Time complexity: O(1)
key에 저장된 value의 size를 리턴한다. key가 string이 아니면 에러를 리턴한다.

- strlenCommand in t_string.c
a. key가 없으면 ":0\r\n"을 client에게 보낸다.
b. key가 string이 아니면  "-ERR Operation against a key holding the wrong kind of value\r\n"을 client에게 보낸다.
c. value의 크기를 client에게 보낸다.

* DEL key [key ...]
Time complexity: O(1)
지정한 키를 지운다. key가 없으면 무시한다.

- delCommand in db.c
a. argument로 지정한 key들을 지운다.
b. 지정한 key를 watch하고 있는 client에게 알린다.
c. 지운 key의 수를 client에게 보낸다.

* EXISTS key
Time complexity: O(1)
key가 존재하는지 확인한다.

- existsCommand in db.c
a. key가 존재하는지 확인한다.
b. 있으면 ":1\r\n"을 client에게 보낸다.
c. 없으면 ":0\r\n"을 client에게 보낸다.

* SETBIT key offset value
Time complexity: O(1)
key에 저장된 value의 offset위치의 bit를 set(1) 또는 clear(0)한다.

- setbitCommand in bitops.c
a. offset의 값을 가져온다. 512MB이하여야 한다. 아닐경우 "-ERR bit offset is not an integer or out of range\r\n"를 리턴한다.
b. value의 값을 가져온다. LONG_MIN과 LONG_MAX사이여야 한다. 아닐 경우 "-ERR bit is not an integer or out of range\r\n"를 client에게 보낸다.
c. offset이 0이나 1이 아니면 "-ERR bit is not an integer or out of range\r\n"를 client에게 보낸다.
d. key를 찾는다.
e. key가 없으면 empty string을 새로 만든다.
f. key가 있으면 type이 string인지 확인한다. 그리고 나서 key가 shared 또는 encoded이면 copy를 하여 기존의 다른 값이 변하지 않도록 한다.
g. string의 크기가 offset만큼 크지 않으면 offset 설정이 가능하도록 string의 크기를 증가시킨다. 이때 추가되는 부분의 bit값은 0으로 한다.
h. offset 부분의 값을 value로 바꾼다.(key의 값이 바뀌므로 watch중인 client에 signal을 보낸다.)
i. 값을 바꾼 bit의 원래 값을 client에게 보낸다.

* GETBIT key offset
Time complexity: O(1)
key에 저장된 value의 offset 위치의 값을 리턴한다.

- getbitCommand in bitops.c
a. offset 값이 정상적인지 확인한다.
b. key가 없으면 ":0\r\n"을 client에게 보낸다. type이 string이 아니면 에러를 리턴한다.
c. key가 있으면 offset 위치의 값을 client에게 보낸다.(":1\\r\n" or ":0\r\n")
d. offset의 위치가 key의 string 크기를 벗어나면 0으로 가정하고 값을 리턴한다.

* SETRANGE key offset value
Time complexity: O(1)
string의 부분을 변경한다.

- setrangeCommand in t_string.c
a. offset값을 가져온다. 값이 0보다 작으면 "-ERR offset is out of range\r\n"을 client에게 보낸다.
b. key를 찾는다.
c. key가 없는 경우 다음의 순서로 진행한다.
    c-1. value의 크기가 0이면 ":0\r\n"을 client에게 보낸다.
    c-2. offset + value의 크기가 최대 크기를 넘으면 에러를 리턴한다.
    c-3. key에 빈 스트링을 넣는다.
d. key가 있는 경우
    d-1. key의 타입이 string인지 확인한다.
    d-2. value의 크기가 0이면 저장되어 있는 key의 value의 크기를 리턴한다.
    d-3. offset + value의 크기가 최대 크기를 넘으면 에러를 리턴한다.
    d-4. object가 shared이거나 encoded이면 object를 복사한다.
e. value의 크기가 0보다 크면 key의 value의 offset위치의 값을 변경한다. 이때 offset위치가 저장되어 있는 value의 크기보다 크면 0(zero byte)을 붙여서 값 변경이 가능하도록 해준다.
f. 변경된 value의 크기를 리턴한다.

* GETRANGE key start end
Time complexity: O(N)
string의 지정한 부분의 값을 가져온다. 음수의 start와 end는 뒤에서 부터를 의미한다. 이전 버전에서는 SUBSTR이었다.(코드에 남아있으므로 사용은 가능하다.)

- getrangeCommand in t_string.c
a. key가 없으면 "$0\r\n\r\n"를 리턴한다.
b. start가 음수인 경우 뒤에서 부터 value에서의 실제 위치를 계산했을 때 value의 0 index보다 더 작게 가면 0으로 설정한다.
c. end가 음수인 경우 뒤에서 부터 value에서의 실제 위치를 계산했을 때 value의 0 index보다 작으면 0으로 설정한다.
d. end가 value의 크기보다 크면 값을 value의 크기로 조정한다.
c. end보다 start가 더 크면 "$0\r\n\r\n"를 리턴한다.

Generic interfaces 요점

 https://go.dev/blog/generic-interfaces  Generic interface를 정의할 때 최소한의 제약만을 정의하고 실제 구현체들이 자신만의 필요한 제약을 추가할 수 있도록 하는 것이 좋다. pointer receiver를...