내가 아는 파이썬의 특징 중 GIL만큼 신기한 특징도 없다.

 

 파이썬에서 하나의 스레드에 모든 자원을 할당하고 다른 스레드에서 접근할 수 없도록 Lock을 건다. OS 공부하며 본 그 뮤텍스와 똑같다. 

이번 포스팅은 Python, Java 두가지 언어로 Single, Multi Thread 각각의 상황에서 무거운 연산을 진행하며 시간을 측정하고 시간 차이를 통해 GIL을 확인해보고 GIL의 존재 이유를 설명하려 한다. 

 

들어가기에 앞서 현재 내 컴퓨터 환경은 아래와 같다.

JDK 8.0, pypy 3.10 을 사용했다. 

 

멀티 스레드 연산 실험 

실험은 1 ~ 2000000000(20억)의 값을 더하는 속도를 비교할 것이다. 

멀티스레드 환경, 단일 스레드 환경의 연산속도를 Java와 Python에서 각각 비교해보겠다. 

 

Java에서 단일스레드

비교를 위한 자바에서의 연산 실험이다.

먼저 단일 스레드에서의 연산을 실험할 코드는 아래와 같다. 

/**
 * 단일 스레드에서의 연산
 * sum : 1~20억을 더한 결과값  
 */
long start = System.currentTimeMillis();
long sum = 0;
for(int i =0; i < 2000000000; i++) {
	sum+=i;
}
long end = System.currentTimeMillis();
System.out.println("Thread");
System.out.println("연산 시간 : " + (end-start)/1000.0 +"초");
System.out.println("result : " + sum);

 

내 훌륭한 노트북은 자바, 단일스레드로 1부터 20억까지 더하는데 0.68초가 걸리는걸 확인했다.

단일 스레드

Java에서의 멀티스레드 

다음은 멀티 스레드에서의 연산을 실험할 코드이다. 

/*
* sum  (long)  : 1부터 20억을 더한 결과
* start(int)   : 나누어진 구간의 시작
* end  (int)   : 나누어진 구간의 끝
*/
public class MyThread extends Thread{
	public static long sum; 
	private int start;
	private int end;
	
	public MyThread(int start, int end) {
		this.start = start;
		this.end = end;
	}
	/**
    	* start ~ end의 값을 모두 더함 
    	*/
	@Override
	public void run() {
		long temp = 0; 
		for(int i = start; i <= end ; i ++) {
			temp+=i;
		}
		sum+=temp; 
	}

}
/**
 * 멀티스레드에서의 연산 
 * target(int) 	  : 1부터 더할 범위의 수 == 20억 
 * tharedCnt(int) : 사용될 스레드의 수 
 * MyThread[]     : 스레드를 담을 변수. 
 * Thread.start() : Thread에서 Thread.run()을 호출함 -> 각 스레드에 할당된 범위를 더
 * Thread.join()  : Thread가 종료될때까지 기다릴때 사용
 *
 */
start = System.currentTimeMillis();
int threadCnt = 8;
int target = 2000000000;
MyThread[] threads = new MyThread[threadCnt];
for(int i = 0 ; i < threads.length; i++) {
	threads[i] = new MyThread(i*(target/threadCnt), (i+1)*(target / threadCnt)-1);
	threads[i].start();
}
for(int i = 0 ; i < threads.length; i++) {
	threads[i].join(); 
}
end = System.currentTimeMillis();
System.out.println("Multi Thread");
System.out.println("연산 시간 : " + (end-start)/1000.0 +"초");
System.out.println("result : " + MyThread.sum);

각 쓰레드는 20억에서 threadCnt개의 범위를 나누어 할당받은 후 static으로 선언된 MyThead.sum의 값에 더하게 된다. 중요한건 결과니깐 결과를 보자.

멀티 스레드

0.68초 -> 0.232초로 빨라졌다.  

 

전체 코드

더보기
JAVA
public class MultiThread {
	public static void main(String[] args) throws InterruptedException {
		/**
		 * 단일 스레드에서의 연산
		 * sum : 1~20억을 더한 결과값  
		 */
		long start = System.currentTimeMillis();
		long sum = 0;
		for(int i =0; i < 2000000000; i++) {
			sum+=i;
		}
		long end = System.currentTimeMillis();
		System.out.println("Thread");
		System.out.println("연산 시간 : " + (end-start)/1000.0 +"초");
		System.out.println("result : " + sum); 
		
		/**
		 * 멀티스레드에서의 연산 
		 * target(int) 	  : 1부터 더할 범위의 수 == 20억 
		 * tharedCnt(int) : 사용될 스레드의 수 
		 * MyThread[]     : 스레드를 담을 변수. 
		 * Thread.start() : Thread에서 Thread.run()을 호출함 -> 각 스레드에 할당된 범위를 더
		 * Thread.join()  : Thread가 종료될때까지 기다릴때 사용
		 *
		 */
		start = System.currentTimeMillis();
		int threadCnt = 8;
		int target = 2000000000;
		MyThread[] threads = new MyThread[threadCnt];
		for(int i = 0 ; i < threads.length; i++) {
			threads[i] = new MyThread(i*(target/threadCnt), (i+1)*(target / threadCnt)-1);
			threads[i].start();
		}
		for(int i = 0 ; i < threads.length; i++) {
			threads[i].join(); 
		}
		end = System.currentTimeMillis();
		System.out.println("Multi Thread");
		System.out.println("연산 시간 : " + (end-start)/1000.0 +"초");
		System.out.println("result : " + MyThread.sum); 
		
	}
}

 

/*
* sum  (long)  : 1부터 20억을 더한 결과
* start(int)   : 나누어진 구간의 시작
* end  (int)   : 나누어진 구간의 끝
*/
public class MyThread extends Thread{
	public static long sum; 
	private int start;
	private int end;
	
	public MyThread(int start, int end) {
		this.start = start;
		this.end = end;
	}
	
	@Override
	public void run() {
		long temp = 0; 
		for(int i = start; i <= end ; i ++) {
			temp+=i;
		}
		sum+=temp; 
	}

}

 

Python에서의 단일스레드 

 정확히는 Pypy를 사용했다. 파이썬의 경우 20억까지 더하는데 예상보다 더 걸린다... pypy는 JIT 컴파일러를 사용하는데 jvm도 JIT 컴파일러를 쓴다. JIT 컴파일러에 대해서는 다음에 자세히 포스팅하기로 하고 아무튼 반복문, 함수 등에 대해 최적화 해주는 친구라고 생각하자.

# start부터 end까지 result에 더해주는 함수
def work(start: int,end: int) -> None:
    global result
    for i in range(start,end):
        result += i

멀티 스레드, 단일스레드도 이 함수를 사용해서 실험한다. 

단일 스레드의 코드는

result = 0
start_time = time.time()
work(1,2000000000)
end_time = time.time() 
print("Single-Thread")
print(f"연산 시간: {(end_time-start_time)/1000}")
print(f"result: {result}")

 

파이썬의 단일 스레드

결과는 위와 같다. 

Python에서의 멀티스레드 

똑같이 작성해놓은 work 함수를 사용할 것이다.

result = 0
start_time = time.time()
threads = []
thread_cnt = 8
target = 2000000000
for i in range(thread_cnt):
    threads.append(threading.Thread(target=work(i * (target // thread_cnt), (i + 1) * (target // thread_cnt))))
    threads[-1].start()

for t in threads:
    t.join()
end_time = time.time()
print("Multi-Thread")
print(f"연산 시간: {(end_time-start_time)/1000}")
print(f"result: {result}")

자바에서 멀티스레드와 동일한 로직으로 동작한다. 

파이썬의 멀티 스레드

결과는 위와 같다. 

 

3.9 -> 4.0초로 더 느려졌다. 자바에서의 결과와 반대로 더 느려졌다.

 

결과

왼쪽 : Java의 단일 스레드(상), 멀티스레드(하)  오른쪽 : Python의 단일 스레드(상), 멀티스레드(하)

자바는 빨라졌지만, 파이썬은 멀티스레드 환경에서 오히려 더 느려졌다. 

 

GIL - Global Interpreter Lock

앞에서 말했지만 파이썬의 경우 하나의 스레드에 자원을 할당하고 Lock을건다. 즉 파이썬 인터프리터는 하나의 스레드만 하나의 바이트 코드를 실행 시킬 수 있다.

 

코어가 8개인 환경에서 스레드 8개가 작업을 한다고 가정해보자. 

파이썬의 멀티 스레드 동작 흐름

000,000,000~250,000,000 까지 더하는 스레드

250,000,001~500,000,000 까지 더하는 스레드

500,000,001~750,000,000 까지 더하는 스레드 

 ... 

1,750,000,000 ~ 2,000,000,000 까지 더하는 스레드

파이썬의 경우 각 스레드는 위 그림과 같이 동작한다. 실험에 빗대어 보면 단일스레드가 3.8초 동안 연산한걸, 8개의 스레드가 4.06초동안 번갈아가며 연산한다. 오히려 context switch에 비용이 추가적으로 발생하며 싱글스레드보다 느린 결과를 보여준다. 자바의 경우엔 이 모든 스레드가 0.232초 사이에 동시에 동작하며 단일 스레드 연산 시간인 0.677초보다 빠르게 해결할 수 있다. 

 

왜 귀도 반 로섬씨는 이런 비효율적으로 보이는 락을 만들었을까. 

 

파이썬은 문자열부터 정수, 실수, 뭐 죄다 객체다. 심지어 함수도 객체다. 

이게 진짜 객체지향이지 ㅋㅋ..

그래서 위 사진처럼 함수도 변수에 저장할 수 있고, 매개변수로도 넘길 수 있다. (이걸 어따쓰냐 궁금하다면.. Decorator 등 한 번 알아봐라)이렇게 모든게 객체인 파이썬은 문제가 조금 있다. 바로 메모리 관리다.

 

 Call By Value랑 Call By Reference도 헷갈려했던 기억이 있는데 파이썬은 Call By Object-reference라는 호출 방식을 사용하고, 알아보니 또 헷갈려 죽으려하는 내가 보인다. 이렇게 헷갈리는데 모든게 객체다. 이때 사람이 메모리 할당과 해제를 직접 한다면 파이썬이 이렇게까지 인기를 끌 수 없었을거다. 아무튼 귀도반로섬씨께서는 메모리 관리 방법으로 reference counting을 사용했다. 예를들면 a = 999라는 코드라면 999라는 객체의 reference counting은 1이다. 이때, 특정 객체를 참조하는 변수의 개수가 0이라면 메모리에서 해제하면 되는데 몹시 간단하고 직관적이다. 물론 단일스레드에서의 얘기다.

 

멀티스레드 환경에서 reference counting은 critical section으로 여러 스레드가 이 영역에 진입하게 되면 Race Condition으로 동기화 문제가 발생한다. 그 결과 값을 참조하고 있음에도 불구하고 reference counting이 0이 되어 메모리에서 삭제될 수도 있고, 아무도 참조를 안하고 있음에도 메모리에 남아있을수도 있다!(파이썬에선 GIL때매 있을 수 없는 상황이으로 극단적인 예시이며 순환참조의 경우는 GC가 세대별로 확인하며 열심히 지울거다.)

 

 어떻게 reference counting의 동기화 문제를 해결할까 고민하던 귀도반로섬은 문제 자체를 뿌리뽑았다. 바로 Race Condition이 일어날 환경자체를 막았다. 파이썬 전체적으로 적용될 뮤텍스를 하나 만들었고 이름은 GIL이라 붙였다! 물론 "아 머리 아프다. 그냥 막아 ㄱㄱ." 이런 판단은 아니다. 동기화를 위해 파이썬 코드의 수 많은 객체와 객체들 간 연산에서 일일히 Lock을 걸어주는것보다 GIL을 사용하는 방법이 훨씬 적은 비용으로 처리할 수 있다. 만약 각각의 객체가 Lock을 갖고 thread-safe하게 만든다면 그 어떤 천재가 와서 코딩해도 DeadLock을 마주하고 "이게 왜 안돼" 하다가 GIL을 만들었을거다. 

 

 

그럼 파이썬을 쓰면 멀티 스레드 환경을 고려 안해도 될까?

 물론 아니다. 기본적으로 GIL은 CPU-bound 작업에 한해 Lock을 건다. Sleep, I/O-bound(입출력, File, DB, Network)에서는  GIL이 적용되지 않는다. 따라서 I/O작업( 네트워크 요청 DB액세스 등)을 수행할때는 멀티 스레딩이 성능적으로 이점을 얻을 수 있다. 그러나 일반적인 I/O 이벤트를 보면 이벤트를 처리하는 비용보다는 이벤트가 발생하기까지 기다리는 비용이 크기때문에 멀티 스레드 보다는 비동기 프로그래밍을 사용하는게 성능적으로 이점을 얻는 경우가 많다. 

 

 CPU-bound의 작업을 수행하는 경우에는 멀티스레드 보다는 멀티프로세스 혹은 다른 병렬처리 방식을 고려해야 한다. 이때 파이썬은 multiprocessing이나, 비동기 작업을 처리하기위한 asyncio와 같은 라이브러리를 제공한다. 싱글 스레드에서 비동기로 작업하기 는 다음에 포스팅하겠다 !.

 

Java는 GIL이 없는데..

Java는 GIL없이 어떻게 메모리를 자동으로 관리하고 있을까? 

파이썬은 Reference Counting을 사용하는 것과 달리 자바에서는 기본적으로 Garbage Collection을 Mark and Sweep 방식으로 관리한다. (JDK 8기준)

 간단하게 설명하면 jvm의 heap 메모리에는 Eden, Survive0, Survive1, Old 영역이 존재한다. 가장 처음 객체가 할당되면 Eden으로, Eden 영역에 메모리가 다 차면 모든 스레드를 일시 정지(Stop-the-world)한 뒤 root객체로 부터 접근 가능한 객체들을 마킹(Mark)하고, 마킹되지 않은 객체는 전부 메모리를 해제(Sweep)한다. 이게 Mark-and-sweep이다. 여기서 마킹된 객체는 살아 남아 survive0 영역으로 이동되며 해당영역까지 가득 찰 경우 다시 한 번 Mark-and-sweep을 진행하고 살아남은 객체는 다른 survive영역으로 이동한다.  일정 횟수의 mark-and-sweep에서 살아남으면 Old 영역으로 넘어가게 된다. 

 Reference Counting 방식은 메모리를 해제하기 위해 객체의 참조가 변경 될 때마다 Critical Section에 진입을 막으며 Atomic한 연산을 수행해야하지만, Mark-and-sweep은 메모리가 가득 찼을때 메모리를 해제하기 때문에 Lock이 필요가 없다. 내비뒀다가 한 번에 해제하기 때문이다. 하지만 마킹을 위해 모든 스레드를 일시정지(Stop-The-world)하는 과정이 존재하고, 객체 소멸 시점을 예측하기도 어렵다는 단점이 있다.(모든 스레드가 일시정지 되는 시점을 예측하기 힘들다.)

 

 

 

 

 


 

 

실험하다가 만난 문제가 많다..

pypy가 실제 동작하는거에 비해 연산시간이 짧게 나오는가 하면 Python은 그냥 하염없이 기다려야했다. 

 

- python의 time.time은 sec를 반환하고 java는 milisec을 반환하기때문에 단위를 맞췄다. "파이썬 빠르네 !" 하고 좋아했다가 아차 싶었다. ㅎㅎ;

 

 

Integer Interning과 Integer Caching

 

x,y에 대해 주소값이 달라질때까지 더하고, 빼봤다.

 

"파이썬은 모든게 객체다." 라고 말하면서 -5~256의 값을 미리 할당해놓고 쓴다는걸 보여주고, 자바와 차이를 보여주려했다. 자바는 미리 할당하지 않는다고 생각했기 때문이다.

그런데 이게 왠걸 자바도 -128~127의 값은 같은 주소값을 바라보고 있었다. 

 

얘넨 이런짓을 왜할까 - 정수 인터닝과 정수 캐싱

 

파이썬의 경우에는 "정수 인터닝 (Integer Interning)" 이라는 최적화 기법이며 작은 정수값은 자주 사용된다는 가정하에 정수객체를 미리 할당하고 지속적으로 재사용하는 방식을 채택했다. 미리 생성하여 캐시에 저장해두고, 동일한 정수 값에 대해 해당 정수 객체를 가리키도록 하며 재사용한다. 이는 정수 객체를 생성하고 해제하는 비용을 줄이고 메모리를 절약할 수 있다. 

 

자바의 경우에는 정수 인터닝과는 조금 다른 "정수 캐싱 (Integer Caching)"을 수행한다. 자세히 말하자면 자바는 Byte, Short, Integer, Long 등의 정수 래퍼 클래스에서, 범위가 -128~127까지인 정수 값에 대해 "정수 캐싱"을 수행한다.  이또한 작은 정수값에 대해 객체를 재사용함으로써 메모리를 절약하고 성능을 향상시키는 것을 목적으로 한다. 

 

 

 

 

 

blue green 배포 방식

실제로 우리팀에서 사용했던 무중단 배포 방식이다.

Spring을 예로 들면 전체적인 흐름은 jenkins를 통해 빌드 후, 빌드 된 jar파일을 배포를 관리하는 폴더로 복사한다. sh 스크립트를 통해 관리하기 되는데 docker compose를 통해 새로운 버전의 컨테이너를 띄우고 이전 버전을 내린다.. 말로는 참 쉽다. ;-; 

블루 그린 배포 - 개념 

블루 그린 배포는 새로운 버전의 인스턴스를 띄운 뒤, 로드밸런서를 통해 새로운 버전의 인스턴스로 트래픽을 요청한다. 

 

블루 그린 배포를 이용하면 기존에 인스턴스를 유지하던 비용의 2배가 들어가는 시점이 존재한다, 또한 새로운 버전에 치명적인 오류가 있다면 전환된 서버는 모두 내려갈 것이다. 하지만 구버전의 인스턴스가 그대로 남아있기 때문에 롤백이 손쉽고, 차마 테스트하지 못한 부분에서 치명적인 오류가 발생했다면 구버전으로 다시 트래픽을 돌리기 간편하다. 

 

블루 그린 배포 - 실습 

1. jenkins pipe line 

		# 스토리 관련 스테이지
	stage('Building story image'){
            steps{
                script {
                  dockerImage = docker.build("jodong2/story-zero-downtime", "/var/jenkins_home/workspace/deploy_test/StoryModule")
                  withCredentials([usernamePassword(credentialsId: 'jodong2', usernameVariable: 'DOCKER_HUB_USERNAME', passwordVariable: 'DOCKER_HUB_PASSWORD')]) {
                     sh 'docker login -u $DOCKER_HUB_USERNAME -p $DOCKER_HUB_PASSWORD'
                     sh 'docker push jodong2/story-zero-downtime'
                  }
                }
            }
        }
       stage('Run docker story image') {
          steps {
              dir('/var/jenkins_home/workspace/deploy/StoryModule'){
                  sh 'chmod +x deploy.sh'
                  sh './deploy.sh'
               }
            }
        }

이미지를 빌드하고 deploy.sh를 실행한다. 

#!/bin/bash

#story deploy

export DOCKER_REGISTRY=jodong2 DOCKER_APP_NAME=story-zero-downtime IMAGE_TAG=latest

EXIST_BLUE=$(docker compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yaml ps | grep Up)
# 블루가 떠있는지 확인 

if [ -z "$EXIST_BLUE" ]; then
    echo "blueis is not exist. so make blue container"
    echo "blue up"
    docker compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yaml up -d
    BEFORE_COMPOSE_COLOR="green"
    AFTER_COMPOSE_COLOR="blue"
    echo "end"
else
    echo "blue is exist. so make green container"
    echo "green up"
    docker compose -p ${DOCKER_APP_NAME}-green -f docker-compose.green.yaml up -d
    BEFORE_COMPOSE_COLOR="blue"
    AFTER_COMPOSE_COLOR="green"
fi

sleep 10

EXIST_AFTER=$(docker compose -p ${DOCKER_APP_NAME}-${AFTER_COMPOSE_COLOR} -f docker-compose.${AFTER_COMPOSE_COLOR}.yaml ps | grep Up)
if [ -n "$EXIST_AFTER" ]; then

    docker compose -p ${DOCKER_APP_NAME}-${BEFORE_COMPOSE_COLOR} -f docker-compose.${BEFORE_COMPOSE_COLOR}.yaml down
    echo "$BEFORE_COMPOSE_COLOR down"
fi

deploy.sh파일은 떠있는 blue, green에 따라 각 docker-compose.yaml파일을 실행하게 된다. 

컨테이너가 띄워지는 시간으로 sleep 10초를 걸어놨는데, spring 메인 서버 같은 경우엔 애플리케이션을 시작하는데 약 20초를 필요로 한다. (사실 무중단임에도 팀원이 잠깐 내려가는거 같은데? 하길래 부랴부랴 수정했다..) 따라서 해당 메인 서버의 sh파일은 sleep이 25초로 설정했다. 

 

이후 다른 색의 컨테이너가 성공적으로 띄워졌다면 이전 컨테이너를 중지한다.

version: '3.7'

services:

  api:
    image: ${DOCKER_REGISTRY}/${DOCKER_APP_NAME}:${IMAGE_TAG}

    container_name: ${DOCKER_APP_NAME}-blue

    environment:
      - LANG=ko_KR.UTF-8

    ports:
      - '{port1}:8080'



    volumes:
      - story-logs:/var/log/app

volumes:
  story-logs:

port의 경우 blue와 green을 다르게 두고 해당 포트를 기준으로 로드밸런서가 구분할 수 있도록 하였다. 

예를들어 blue는 8081, green은 8082를 열어둔다. aws에서 보안설정도 잘 확인하자.... ;-; 

# 문제가 좀 있는.. upstream 구성.. 
upstream story {
    least_conn;
    server {ip}:{blue port1};
    server {ip}:{green port2};
    server {ip}:{blue port1};
    server {ip}:{green port2};
}

Nginx에서 upstream을 통해 blue green 각 서버로 연결한다. 

 이외에도 헬스체크 기능을 활용해서 죽은 서버에는 요청을 할 수 있지만 연결을 실패할 경우 nginx는 정해진 알고리즘에따라 다음 서버로 요청을 보낸다. 따라서 별도의 설정 없이도 올라가있는 컨테이너에 요청을 보낼 수 있다. 가장 처음 시도한 방식이고,,, 죽은 서버에 요청을 보내고 실패를 돌려받은 뒤 다음 서버에 요청을 보내는건 문제가 있는 방식이다.

 

Nginx - Health Check 

죽은 서버로 요청을 보내지 않기 위해 health check하는 방법이 있다. 서버가 정상 상태인지 실시간으로 계속 확인하는 방법으로 정상서버의 서버에게만 트래픽을 분배한다. 

upstream story {
    least_conn;
    server {ip}:{blue port1};
    server {ip}:{blue port1};
    check interval=3000 rise=2 fall=5 timeout=4000 type=http;
    check_http_send "HEAD / HTTP/1.0\r\n\r\n";
    check_http_expect_alive http_2xx http_3xx;
}

추가된 내용을 보면 3초단위로 2번 성공하면 정상, 5번 실패하면 비정상으로 판단한다. health check는 http프로토콜을 사용하고 HTTP1.0을 사용하여 보낸다. HTTP1.1~2를 주로 사용하지만 1.0을 사용한 이유는 연결에 성공해도 지속하지 않고 바로 끊는 특징을 갖고 있어서 1.0을 사용했다. 또한 응답코드가 2xx, 3xx라면 정상상태로 판단한다.

죽은서버에 요청을 보내며 네트워크에 지연이 발생할 수 있었던 설정을 위와 같이 죽은 서버에는 요청을 보내지않도록하는 Nginx의 health check기능을 사용하는 방법이 있었다.

 

 

2개의 nginx 설정파일과 deploy.sh 변경

docker compose로 컨테이너를 삭삭(팀원말투인데 머리속에 떠오르 짧고 간결한 표현이 이거밖에 안떠오른다ㅂㄷㅂㄷ; ) 바꾸는데 Nginx도 같이 갈아끼우고 reload하면 되지 않을까? reload하는 순간 짧은 down time이 있지만 health check한다고 자원을 계속 소모하는 방법보단 짧은 down time으로 자원을 아낄 수도 있다. 

 

변경된 deploy.sh이다.

#!/bin/bash

#story deploy

export DOCKER_REGISTRY=jodong2 DOCKER_APP_NAME=story-zero-downtime IMAGE_TAG=latest

EXIST_BLUE=$(docker compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yaml ps | grep Up)
# 블루가 떠있는지 확인 

if [ -z "$EXIST_BLUE" ]; then
    echo "blueis is not exist. so make blue container"
    echo "blue up"
    docker compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yaml up -d
    BEFORE_COMPOSE_COLOR="green"
    AFTER_COMPOSE_COLOR="blue"
    echo "end"
else
    echo "blue is exist. so make green container"
    echo "green up"
    docker compose -p ${DOCKER_APP_NAME}-green -f docker-compose.green.yaml up -d
    BEFORE_COMPOSE_COLOR="blue"
    AFTER_COMPOSE_COLOR="green"
fi

sleep 10

EXIST_AFTER=$(docker compose -p ${DOCKER_APP_NAME}-${AFTER_COMPOSE_COLOR} -f docker-compose.${AFTER_COMPOSE_COLOR}.yaml ps | grep Up)
if [ -n "$EXIST_AFTER" ]; then
	sudo ln -sf /etc/nginx/sites-available/nginx.${AFTER_COMPOSE_COLOR}.conf /etc/nginx/sites-enabled/nginx.conf
        sudo service nginx reload
    docker compose -p ${DOCKER_APP_NAME}-${BEFORE_COMPOSE_COLOR} -f docker-compose.${BEFORE_COMPOSE_COLOR}.yaml down
    echo "$BEFORE_COMPOSE_COLOR down"
fi

다음 색깔의 컨테이너가 제대로 떴다면 다음색깔에 맞는 nginx 설정파일로 변경한뒤 reload 한다. 

reload하는 과정에서 짧은 down time이 발생할 수 있어서 무중단배포라기엔 칭호가 조금 아쉽지만 처음 생각한 방식은 이렇다.. 

 

nginx의 설정파일은 

upstream story {
    least_conn;
    server {ip}:{port1};
}

이렇게 간결해졌고 그에맞는 ip:port만 다른 파일을 설정해뒀다. 

server가 한대라면 location에서 바로 쏴도 될 듯 싶다 ! 

 

 


 

저번주에 올렸어야됐는데.. 할 게 많았다. 

프로젝트하랴 CS공부하랴..싸피 마지막 플젝이 끝났다 ! 

끝나도 시키는게 뭔가 많다. 서버 관련 설정파일은 나중에 다시 보기위해 저장해놔야되는데.. 아이고. .언제다하지.. 

다음주 주말에는 별일이 없다면 자바나 파이썬에 관련된 글을 올릴듯 싶다. 

 왜냐면.. 저번주에 자바에 SOLID에 대해 공부했었다. 옛날옛적에 면접준비한다고 부랴부랴 공부했을때는 이해가 잘 안됐는데 이번엔 "아이고 내 코드는 객체지향을 개떡같이 사용하고 있구나!" 를 느낄 수 있었다. 그래서 다음에 진행하게 될 프로젝트나 기회가 된다면, 지금까지 해왔던 프로젝트 하나를 잡고 수정해볼 생각이다. 

 파이썬을 포스팅한다면... 파이썬의 GIL에 대해 시간을 길게 잡고 자세하게 포스팅해볼 생각이다..

 

 

 

문제

제한사항

입출력 예 

ret : 3

풀이

import java.util.*;
class Solution {
	/*
    * 시간을 모두 분단위로 바꿈 
    * 종료시간은 청소해야되니까 +10분 
    */
    public int[] HtoM(String[] hour){
        int[] ret = new int[2]; 
        StringTokenizer stk ;
        for(int i =0 ; i <2; i ++){
            stk = new StringTokenizer(hour[i],":");
            ret[i] = 60*Integer.parseInt(stk.nextToken()); 
            ret[i] += Integer.parseInt(stk.nextToken()) + i*10; 
        }
        return ret;
    }
    
    public int solution(String[][] book_time) {
        int answer = 0;
               
        int[][] timeLine = new int[book_time.length][2];
        int i = 0; 
        for(String[] book : book_time){
            timeLine[i++] = HtoM(book);
        }
        
        // 입실 시간을 기준으로 정렬 
        Arrays.sort(timeLine, new Comparator<int[]>(){
            @Override
            public int compare(int[] a1, int[] a2){
                return a1[0] == a2[0] ? a1[1]-a2[1] : a1[0]-a2[0];
            }
        });
        
        // 퇴실시간이 빠른것부터 퇴실
        PriorityQueue<int[]> pq = new PriorityQueue<>((o1,o2)->{
            return o1[1] == o2[1] ? o1[0]-o2[0] : o1[1] - o2[1];
        });
        
        int cnt = 0; // 현재 방 갯수 
        int maxCnt = 0; // 최대였던 방 갯수 
        
        for(int[] time : timeLine){
            int[] last;
            while(!pq.isEmpty()){
                last = pq.poll(); 
                cnt -=1;
                if(time[0] < last[1]){
                // 가장 빠른 퇴실시간이 지금 손님 입실시간보다 느리면 방 추가.
                    pq.add(last);
                    cnt+=1;
                    break; 
                }
            }
            pq.add(time); 
            cnt+=1;
            maxCnt = Math.max(cnt,maxCnt);
        }
        return maxCnt;
    }
    
    /**
    * 디버깅용 print function
    */
    public void sysout(int[] a){
        System.out.println(""+a[0] + " "+ a[1]);
    }
}

 

 

느낀점 - "파이썬 VS 자바"

자바로 알고리즘을 안푼지 오래돼서 세미콜론때매 자꾸 오류가 났다;

프로그래머스는 자동완성도 안되니까 죽을맛이다.

 

알고리즘 문제를 풀 때 파이썬이 훨씬 편하다. 

 

예를들어 위 문제에서 2차원 배열을 0번 index기준으로 정렬할때

자바는 아래와 같이 정렬할 수 있다.

Arrays.sort(arr, new Comparator<int[]>({

    @Override
    public int compare(int[] o1, int[] o2){
    	return o1[0]==o2[0] ? o1[1]-o2[1] : o1[0]-o2[1];
    }
    
}));

파이썬은 아래와 같이 정렬할 수 있다. 

arr.sort(key= lambda x : x[0])

 

 

자바에서 저거보다 쉽게 2차원 배열을 정렬하는 법 있으면 알려주세여,.

 

 

우선순위큐도 역시 파이썬이 간단하다

위에 문제처럼 timeline 배열의 1번인덱스 변수를 기준으로 최소힙을 사용하고 싶다면

자바는 아래와 같이 pq를 만든 후 add를 진행하면 된다. 

PriorityQueue<int[]> pq = new PriorityQueue<>((o1,o2)->{
	return o1[1] == o2[1] ? o1[0] - o2[0] : o1[1] - o2[1]; 
});

pq.add(new int[]{1,2});
pq.add(new int[]{2,3});

 

 

 

 

파이썬은 heapq 모듈을 아래와 같이 사용하면 된다. 

arr =[] 
heapq.heappush(arr, (a[1],a))
heapq.heappush(arr, (b[1],b))

_, first = heapq.heappop(arr)
_, second = heapq.heappop(arr)

 

 

번외로 최소힙이 아닌 최대힙을 만들고 싶다면 자바는 lambda 식의 파라미터로 들어온 o1, o2중 뒤의 값에서 앞의 값을 빼면 된다. 

PriorityQueue<int[]> pq = new PriorityQueue<>((o1,o2)->{
	return o1[1] == o2[1] ? o2[0] - o1[0] : o2[1] - o1[1]; 
});

 

파이썬은 최대힙을 만들기 위해 음수를 한 번 곱해준다. 

arr =[] 
heapq.heappush(arr, (-a[1],a))
heapq.heappush(arr, (-b[1],b))

_, first = heapq.heappop(arr)
_, second = heapq.heappop(arr)

 

 


 

다음은 자바 익명객체하고 무중단배포 포스팅해야쥐ㅣㅇ

 

 

+ Recent posts