InitializeCriticalSectionAndSpinCount, CriticalSection에 대한 정보 이것저것

개인적으로 만들고 있는 프로젝트, 프로그램들은 대부분 리눅스/윈도우간의 크로스플랫폼을 지향하고 있는데 그래서 그런지 락을 쓸 때는 std::mutex를 자주 쓰고 있다.

최근에는 윈도우의 critical section이 더 빠르다는 얘기가 자꾸 들려서 윈도우일 때는 critical section을 더 쓰도록 변경하는 중.

그러다가 InitializeCriticalSection 이라는 초기화함수 대신 InitializeCriticalSectionAndSpinCount 라는 함수를 보게 되었는데 이게 무엇인지 찾아보았다. 이 함수의 인자는 2개인데 첫번쨰는 당연히 크리티컬섹션 객체의 포인터이고 뒤에 붙는 인자는 스핀 횟수이다.

어떤 스레드가 작업을 하다가 크리티컬섹션에 걸려서 InitializeCriticalSection 을 썼다면 대기상태에 들어가게 된다. InitializeCriticalSectionAndSpinCount 를 썼다면 바로 대기상태에 들어가지 않고 인자로 줬던 SpinCount 횟수만큼 기다리게 된다. 만약 그 사이에 크리티컬섹션이 풀리면 바로 다시 작업 진행. SpinCount 횟수만큼 기다렸는데도 크리티컬섹션이 풀리지 않았다면 대기상태로 들어간다.

InitializeCriticalSectionAndSpinCount는 멀티프로세서 시스템에서만 유효하며 싱글프로세서 시스템에서는 뒤에  SpinCount 값은 무시되고 0으로 설정된다. 멀티프로세서 시스템인 경우, 크리티컬섹션에 걸려 대기에 들어가지 않으므로써 스레드간 Context Switching이 적어지므로 결과적으로 InitializeCriticalSection 을 사용할 때보다 성능이 더 나아질 수 있다.

또한 InitializeCriticalSection 의 리턴은 void 이기 때문에 크리티컬섹션 초기화가 제대로 되었는지 알 수가 없다. 메모리 부족 상황 등에서 초기화 실패가 일어날 수 있고 이 경우 InitializeCriticalSection 함수 부분에서 exception이 일어나게 되어 추적하기 힘든 버그가 발생할 수 있다. InitializeCriticalSectionAndSpinCount 함수는 리턴값이 있고 이러한 버그 상황을 피할 수 있도록 만들어준다.

예제 코드

CRITICAL_SECTION g_cs;
int main()
{
    if (!InitializeCriticalSectionAndSpinCount(&g_cs, 4000))
    {
        // 예외 처리
    }
    ...
}
// [출처] [VC++] 크리티컬 섹션 초기화 관련 간단 팁|작성자 데브머신

이와 관련하여 스핀카운트의 단위가 무엇인지, 스레드 컨텍스트 스위칭이 이루어지지 않고 크리티컬섹션이 풀릴 때까지 어디서 어떻게 기다리는지 더 알아보고 싶지만 구글을 뒤져봐도 자료가 없는듯하다.

<참고자료>

메모리복사시 침범 문제

boost asio를 이용하여 코드를 작성 중에 다음과 같은 부분이 있었다.

클래스의 멤버변수로 네트워크 처리에 사용할 버퍼를 만들고 변수를 생성한 코드다.

class BasicSocket : public std::enable_shared_from_this<BasicSocket>
{
// 중략
protected:
    std::shared_ptr<Socket> socket_ptr_;

    Socket socket_;

    char packet_buffer_[RECV_BUFFER_SIZE * 2];
    int32_t remain_size_;

    char recv_buffer_[RECV_BUFFER_SIZE];
    char send_buffer_[SEND_BUFFER_SIZE];
};

이후 이 변수들을 사용해서 다음과 같은 코드를 작성했다.

std::cout << "OnReceive 1 remain_size_:" << remain_size_ << " - bytes_transferred:" << bytes_transferred << std::endl;

// 패킷을 담아둘 버퍼에 수신버퍼의 내용을 복사한다.
memcpy(&packet_buffer_[remain_size_], recv_buffer_, bytes_transferred);

std::cout << "OnReceive 2 remain_size_:" << remain_size_ << " - bytes_transferred:" << bytes_transferred << std::endl;

데이터 수신 후 처리하는 부분인데 단순히 packet_buffer에 memcpy를 하기만했는데 remain_size의 값이 변화한다는 사실.

위에서 출력된 remain_size와 밑에서 출력된 remain_size의 값이 다르다는 것이었다. 더 문제는 이 출력이 어떤 경우에는 정상적이고 어떤 경우에는 다르게 출력되었다. remain 값을 변화시키는게 없는데 왜 그럴까 한참 살펴보다가 VS2015의 메모리 디버거를 보다가 허탈하게 이유를 알아냈다.

remain_size가 할당된 위치가 packet_buffer의 바로 다음 메모리 공간이었다. 메모리 복사시, recv_buffer와 packet_buffer 크기가 같고 remain_size가 0보다 크다면, packet_buffer 바로 다음 메모리 공간(=remain_size가 할당된 위치)까지 메모리 복사가 일어나서 오염되었던 것.

그런데 이와 동일한 코드를 쓴 다른 서버는 같은 코드임에도 에러가 일어나지 않았다. 같은 코드더라도 프로그램마다 메모리 공간 할당이 다르게 이루어진다는 것으로 이해해야할까.

boost::property_tree의 위험성

.ini 파일을 파싱하기 위해 boost::property_tree 클래스를 사용했었다.

서버의 성능을 테스트하던 도중 어디사 CPU를 많이 잡아먹나 계속 테스트했는데 네트워크 전송 부분이 계속 문제였다. 아무리 수정을 해도 고쳐지지 않는 상황. 비주얼스튜디오의 성능프로파일러를 돌려보니 boost::property_tree 가 문제의 원인이었다. 환경설정에서 특정한 값을 가져오기 위해 쓰느 코드가 네트워크 속도를 느리게 만드는데 기여하고 있었다.

이 부분을 수정하고나니 CPU 점유율이 아주 낮아졌다.

boost를 마구 쓰면 안된다는걸 깨닫게되었네.

boost에서 메모리풀 사용

프로그램 구현 중 메모리풀이 필요한 경우가 있어 어떻게 할까 하다가 boost의 메모리풀을 찾아보게 되었다.

boost의 메모리풀은 4가지.

  1. pool
  2. singleton_pool
  3. pool_alloc
  4. object_pool

네가지 메모리풀 중에 어떤 것이 어떤 상황에 가장 적합한지는 많이 써봐야 알 수 있을듯하다. 찾아보니 내가 가지고 있던 몇몇 게임서버 소스는 singleton_pool을 사용하고 있는 케이스가 있었다. 이것부터 먼저 찾아보는 것으로.

singleton_pool은 다른 메모리풀 클래스들과는 다르게 스레드-세이프 특성을 지니고 있어서 가장 유용하게 쓰일 것 같다.

singleton_pool을 아래와 같은 코드로 좀더 편하게 사용 가능. 출처는 ‘아지크의 좌충우돌 IT 이야기’

#include <stdio.h>
#include <iostream>
#include <vector>
#include <boost/pool/singleton_pool.hpp>
#include <boost/pool/pool_alloc.hpp>


template<typename T,  unsigned int NEXT_SIZE = 32U, unsigned int  MAX_POOL_SIZE = 0U>
class CMemoryPoolT
{
public:
     static void* operator new(size_t size)
     {
          return boost::singleton_pool<T,
               sizeof(T),
               boost::default_user_allocator_new_delete,
               boost::details::pool::default_mutex,
               NEXT_SIZE,
               MAX_POOL_SIZE>::malloc();
     }

     static void operator delete(void* p)
     {
          boost::singleton_pool<T,
               sizeof(T),
               boost::default_user_allocator_new_delete,
               boost::details::pool::default_mutex,
               NEXT_SIZE,
               MAX_POOL_SIZE>::free(p);
     }
};


class CMemoryPoolTest  :public CMemoryPoolT<CMemoryPoolTest>
{
public:
     char Dumy[124] ;
};


int _tmain(int argc, _TCHAR* argv[])
{
     using std::vector ;

     CMemoryPoolTest *p = new CMemoryPoolTest() ;

     delete p ;

     printf("Using MemoryPool... \n") ;


     return 0;
}

프로그램 종료시에는 purge_memory()를 꼭 호출해주어야 한다고 한다.

혹은 다음과 같은 방법으로도 사용 가능하다.

struct SEND_BUFFER_TAG {};
typedef boost::singleton_pool<SEND_BUFFER_TAG, SEND_BUFFER_SIZE> SendBufferPool;

사용할 클래스의 헤더파일에서 위와 같이 선언. TAG를 여러개 사용하고 메모리풀마다 다르게 적용하면 서로 다른 메모리풀이 된다.

메모리를 할당 받고 싶다면,

char* send_data = static_cast<char*>(SendBufferPool::malloc()); // 메모리를 할당 받는다.

SendBufferPool::free(send_data); // 다 사용한 메모리를 해제한다.

종료시에는 당연히,

SendBufferPool::purge_memory();

를 호출해야한다.

출처와 참고사이트

 

Github 에서 내용에서 민감한 정보 삭제하기

github에 소스코드를 올리면서 중요한 코드를 삭제하지 않고 올렸다는걸 나중에서야 알게되었다. github에서 코드를 지워야하는 상황.

구글에 검색해보니 몇가지 답이 나왔다. 명령어를 입력해서 처리하는 방법과 bfg라는 툴을 이용하는 방법 두가지가 있는데 일단 bfg는 좀 귀찮았고(많은 설명들에서 bfg가 무엇인지 안나와있는데 github에서 bfg를 검색해보면 된다.) 명령어를 입력해서 처리해보기로 결정.

여튼 검색된 문서의 이런저런 방법대로 해봐도 잘 안되서 마지막은 github에서 공식적으로 안내된 문서대로 해보았더니 바로 적용되었다. (https://help.github.com/articles/removing-sensitive-data-from-a-repository)

일단 삭제할 내용이 포함된 파일을 백업한 다음, 입력할 명령어는 다음과 같다.

git filter-branch --force --index-filter 'git rm --cached --ignore-unmatch 삭제될파일의_전체경로' --prune-empty --tag-name-filter cat -- --all

이것을 입력하고 일단 무언가 텍스트들이 잠시 지나가길 기다린 다음, 마지막으로 push를 하면 된다. 삭제될 파일의 경로는 정확하게 경로 모두와 파일명 전부를 적어줘야한다.

git push origin --force --all

그리고 나면 해당 파일이 삭제되어 있다. 백업했던 파일을 수정한 다음 원래 위치로 다시 복구하고, add, commit, push를 차례대로 해준다.

그러면 복구 완료.

Lightsail 로 서버 이전 후기

계속 사용해오던 cloudv의 가상서버에서 aws의 새로운 서비스인 lightsail로 이전했다.

굳이 웹호스팅을 쓰지 않고 가상서버로 구성해서 사용하는 것은 git 저장소도 한몫했는데 일단 혼자서 사용하다보니 뭐하러 이런걸 해야하나…라는 회의감과 그럴거면 차라리 github에서 사람들과 같이 코드를 만들고 올리고 하는게 더 낫겠다는 생각이 들었기 때문이었다. 그래서 차라리 웹데이터들은 더 저렴한 lightsail로 이전하고 git 데이터들은 github.com 으로 이전하기로 했다.

lightsail은 사용해보니 그냥 흔히 알고있는 가상서버다. 우리나라에서는 이미 많이 서비스하고 있는.

그런데 가격이 생각보다 싸다. 내 경우에는 월 5달러의 제일 저렴한 리눅스 서버로 골랐다. 메모리 512MB에 20GB의 디스크다. 디스크야 어차피 git 데이터가 github로 넘어간 이상 많이 필요치 않았지만, 너무 적은 메모리와 알 수 없는 cpu 성능 때문에 고민을 했다. 고민 끝에 최대한 메모리를 적게 쓰도록 아파치를 쓰지 않고 nginx로 쓰기로 했다.

그리고 어차피 방문객이 많지도 않고 방문객이 많으면 인스턴스를 늘리면 끝이니까.

bitnami의 여러 스택이 있었는데 사용하기 편하게 wordpress 스택을 선택하려 했지만 이 경우에는 멀티사이트를 만들기에 불편할 것 같아서 그냥 nginx 스택을 선택하고 직접 설정해서 아내의 홈페이지와 내 블로그 두개를 설치했다. nginx 설정 파일이 익숙치 않아서 약간 헤매었지만 금방 적응했다. bitnami류의 패키지 시스템이 마음에 들지 않았지만 직접 써보니 생각보다 괜찮았다. 멀티앱/멀티사이트간 여러 설정 파일들이 서로 꼬이지 않게 잘 정리되어 있었다. (물론 그런 이유로 인해 뭔가 하나 고치려면 한참 경로를 타고 들어가야 한다는건 불편하다.)

여튼 그렇게 설치하고 아파치에서 nginx로 변경했는데 와우… 속도가 생각보다 괜찮았다. 내가 아파치로 혼자 가상서버를 쓰는 것보다 훨씬 빨랐다.

접속방법에서 약간의 불편함은 있지만 어차피 접속할 일도 별로 없고… 게다가 1TB의 트래픽은 트래픽 걱정할 필요는 아예 없을만큼 정말 여유로웠다.

며칠간 여러 데이터들을 이전해본 후기

  1. 생각보다 속도가 빠름.
    도쿄 리전이고 가상서버에다가 메모리도 겨우 512MB라서 느릴 것으로 예상했지만 기존 서버보다 훨씬 빠른 느낌. 아파치와 엔진엑스의 차이인지도…
  2. 시간대 재설정 필요함.
    설치하게 되면 서버 시간을 한국에 맞춰야한다. ( https://gist.github.com/dongbum/1673616e33fb331ff8e876ee62216988 으로 저장해둔다.)
  3. 설정파일 트리구조를 잘 파악해둘 것.
    엔진엑스에 익숙치 않거니와 구조가 좀 복잡하긴하다.
  4. DNS 응답 속도가 아주 빨라짐.
    BIND나 PowerDNS로 네임서버를 직접 구성해서 썼었는데 그렇게 쓰는것보다 lightsail에서 제공하는 DNS의 응답속도가 훨씬 속도가 빨랐다. 거의 100배 정도.
  5. lightsail의 DNS는 Route 53과 약간 다름.
    lightsail에서도 300만 쿼리까지는 무료로 DNS를 제공해주는데 사용하기는 너무 편하게 되어있지만 기능적인 면에서는 Route 53 보다는 떨어진다.
  6. 512MB의 메모리로도 충분함.
    가장 저렴한 512MB 메모리의 인스턴스로도 블로그와 몇가지 개인서비스를 운영하는데에는 차고 넘치는 느낌이다.
  7. 사용상 몇가지 불편함이 있음.
    bitnami 패키지의 특성인지 보안을 위해 암호 같은게 어렵게 설정되어 있어서 약간 불편함. 암호를 일일히 바꾸고 쓰기보다는 그냥 쓰기로 결정했다.

 

2018년 5월 17일 업데이트

얼마전 Lightsail 에도 서울 리전이 생겼다. 한국 사람들은 서울 리전 쓰면 될듯. 난 그냥 귀찮아서 계속 도쿄에 내버려두는 중.

2018년 6월 27일 업데이트

스냅샷을 생성하여 서울 리전으로 옮기려 했지만 스냅샷으로 인스턴스 생성은 같은 리전으로 밖에 되지 않았다. 리전간 스냅샷 전송기능이 생길 때까지 기다려야 할듯하다.

2018년 12월 3일 업데이트

리전간 스냅샷 복사기능이 생겨났다!!! 몰랐는데 아래 카리스턱님의 댓글로 알게되었다. 하지만 이 기능이 없어서 이미 난 서울 리전에 인스턴스를 생성해서 수동으로 모두 이전했다. 외국을 대상으로 서비스하다가 한국으로 이전하거나 혹은 그 반대일 때 아주 유용한 기능일듯.

오픈소스 라이센스 검색사이트

라이센스를 검색해보다가 발견한 사이트.

아마 프로그래머들이라면 특정 오픈소스 라이브러리를 가져다가 쓸 일이 많을텐데 이놈의 오픈소스들은 규격화된 라이센스조차도 수십가지인지라 도무지 이게 뭔내용인지 기억할래야 할수가 없다.

Boost 라이브러리를 쓰다가 Boost 라이센스라는게 있길래 이 라이센스는 도대체 내용이 뭐인지 찾다가 발견했다.

부스트 라이센스는 다음과 같았다,

https://olis.or.kr/license/Detailselect.do?lId=1070

각종 오픈소스 라이브러리를 사용할 때마다 한번씩 참고해봐야겠다.

CentOS 7에 plexpy 설치

plex 서버를 잘 사용 중인데 plexpy라는게 있다고 한다. 대강 보니 모니터툴 같은데 일단 리눅스에서 plexpy 를 설치해본다. 인터넷에 있는 몇몇 문서들은 도커를 이용한 설치를 예시로 하고 있는데 내 서버들은 아직 도커를 쓰지 않으므로 그냥 설치하는 것으로.

리눅스는 CentOS 7. plexpy 홈페이지에 들어가 InstallGuide 문서를 읽어본다.

https://github.com/JonnyWong16/plexpy/wiki/Installation

GIT을 이용하여 소스 코드를 다운 받는다.

git clone https://github.com/JonnyWong16/plexpy.git

[root@localhost source]# git clone https://github.com/JonnyWong16/plexpy.git
 Cloning into 'plexpy'...
 remote: Counting objects: 15652, done.
 remote: Total 15652 (delta 0), reused 0 (delta 0), pack-reused 15652
 Receiving objects: 100% (15652/15652), 77.18 MiB | 2.67 MiB/s, done.
 Resolving deltas: 100% (8905/8905), done.
 [root@localhost source]#

설치문서에는 /opt 에 설치하는 것을 예로 들고 있지만 이 경로를 난 좋아하지 않으므로 /usr/local/plexpy 에 설치할 예정.

다운 받은 내용을 /usr/local/plexpy 로 이동한다.

[root@localhost source]# mv plexpy /usr/local/
 [root@localhost source]# cd /usr/local/plexpy
 [root@localhost plexpy]#

plexpy.py를 실행하면 StandAlone 으로 실행하는 것 같지만 난 데몬서비스가 더 좋으므로 관련 문서를 읽어본다.

https://github.com/JonnyWong16/plexpy/wiki/Install-as-a-daemon

리눅스인 경우 다음의 문서를 읽어보라고 한다.

https://github.com/JonnyWong16/plexpy/blob/master/init-scripts/init.systemd

보여지는 스크립트 파일 내용을 vi 를 이용해 plexpy.service 파일로 입력한다.

파일 안에 있는 /opt 로 시작하는 경로들을 나에게 맞도록 /usr/local q로 시작하도록 수정한 다음 저장한다. 실행 유저과 그룹도 plexpy 로 변경.

.service 파일은 보통 /lib/systemd/system 에 저장한다고 하니 그곳으로 옮겨주고 스크립트를 재로딩한 다음, 제대로 읽혀지는지 테스트해본다.

[root@localhost plexpy]# mv plexpy.service /lib/systemd/system
 [root@localhost plexpy]# systemctl daemon-reload
 [root@localhost plexpy]# systemctl status plexpy.service
 ● plexpy.service - PlexPy - Stats for Plex Media Server usage
 Loaded: loaded (/usr/lib/systemd/system/plexpy.service; disabled; vendor preset: disabled)
 Active: inactive (dead)
 [root@localhost plexpy]#

서비스를 시작한다.

[root@localhost plexpy]# systemctl start plexpy.service
 Job for plexpy.service failed because the control process exited with error code. See "systemctl status plexpy.service" and "journalctl -xe" for details.
 [root@localhost plexpy]#

 

에러가 난다. 에러메시지대로 status 명령을 입력해본다.

[root@localhost plexpy]# systemctl status plexpy.service
 ● plexpy.service - PlexPy - Stats for Plex Media Server usage
 Loaded: loaded (/usr/lib/systemd/system/plexpy.service; disabled; vendor preset: disabled)
 Active: failed (Result: exit-code) since 화 2017-09-12 14:24:11 KST; 1min 10s ago
 Process: 23144 ExecStart=/usr/local/plexpy/PlexPy.py --quiet --daemon --nolaunch --config /usr/local/plexpy/config.ini --datadir /usr/local/plexpy (code=exited, status=217/USER)

9월 12 14:24:11 localhost.localdomain systemd[1]: Starting PlexPy - Stats for Plex Media Server usage...
 9월 12 14:24:11 localhost.localdomain systemd[1]: plexpy.service: control process exited, code=exited status=217
 9월 12 14:24:11 localhost.localdomain systemd[1]: Failed to start PlexPy - Stats for Plex Media Server usage.
 9월 12 14:24:11 localhost.localdomain systemd[1]: Unit plexpy.service entered failed state.
 9월 12 14:24:11 localhost.localdomain systemd[1]: plexpy.service failed.
 [root@localhost plexpy]#

 

설치 문서를 읽어보니 환경설정에 대한 내용을 하나도 실행 안한 것이었다.

유저를 추가하고 권한을 준다.

[root@localhost plexpy]# adduser --system --no-create-home plexpy
 [root@localhost plexpy]# chown plexpy:plexpy -R /usr/local/plexpy
 [root@localhost plexpy]#

 

이제 다시 시작해보면 에러가 나지 않는다.

[root@localhost plexpy]# systemctl start plexpy.service
 [root@localhost plexpy]#

 

서비스를 자동실행으로 등록한다.

[root@localhost plexpy]# systemctl enable plexpy.service
 Created symlink from /etc/systemd/system/multi-user.target.wants/plexpy.service to /usr/lib/systemd/system/plexpy.service.
 [root@localhost plexpy]#

 

웹브라우저로 서버의 8181 포트를 열어보면 다음과 같은 화면이 나온다. 이제부터는 웹에서 설정!

그런데 막상 설치해서 보니까 이게 뭐… 딱히 모니터링이라 하기도 애매하고… 어디다 써야할지는 잘 모르겠다. 아파치에 같이 물릴려고 했는데 귀찮아서 그냥 쓰는 것으로.

리눅스에서 스왑 메모리 관리하기

가상서버로 이사한 이후 메모리가 너무 적은지 계속 메모리 관련 에러가 난다.

passenger 모듈 설치에도 메모리에러 때문에 잘 안되었었는데 redmine + git을 사용하려고 하니 또 메모리가 너무 적다고 안된다고 한다. 아무래도 스왑메모리를 2기가 정도 지정해놓고 계속 써야할 모양.

dd if=/dev/zero of=/swap bs=1024 count=2048000

명령으로 2기가짜리 빈 파일을 만든다.

mkswap /swap

명령으로 스왑 생성

swapon /swap

명령으로 스왑 활성화

/etc/fstabs 에 추가

/swap swap swap defaults 0 0