Go 서버 개발하기

  • |
  • 12 February 2018
Image not Found

Go 서버 개발을 시작하며

 

특정 API만 다른 언어로 구현해서 최대의 성능을 내보자!

저희 서버는 대부분 Django framework 위에서 구현된 광고 할당 / 컨텐츠 할당 / 허니스크린 앱 서비스 이렇게 나눌 수 있는데 Python 이라는 언어 특성상 높은 성능을 기대하기가 어려웠습니다. 하지만 세가지 서비스에서 락스크린에서 어떤 컨텐츠나 광고를 보여줄지 결정하는 Allocation(할당) API 가 가장 많이 호출되고 있었는데 빈도로 보면 80% 정도로 높은 비중을 차지하고 있어서 이 Allocation API 들을 성능이 좋은 다른 언어로 구현하면 어떨까 하는 팀내 의견이 있었습니다.

Why Go?

저는 예전부터 Java,  C# 등의 컴파일 언어에 익숙해서 기존 Java 와 C, 그리고 Go 라는 최근에 새로 나온 언어 중에서 아래 블로그글과 같이 여러 reference 들을 통해 성능이 좋다는 Go 로 이 API 들을 포팅하는 작업을 시작하게 되었습니다. Go 에 대한 첫 인상은 Java, C계열 언어보다 덜 verbose 보였고 python 보다는 strongly-typed, encapsulated 하다보니 자유도를 제한해서 코드를 보기 쉽게 하는 것을 선호하는 저의 성격과도 잘 맞는 언어였습니다.

출처: Carles Mateo, Performance of several languages

서버 개발 환경

 

Server design

How to import libraries

  • GVT (https://github.com/FiloSottile/gvt) - Go 는 vendering tool 을 통해 dependency 를 관리할 수 있습니다. GVT 의 경우 처음 도입했을 때 별로 유명하지 않았는데 사용법이 간단해서 도입하게 되었습니다. 아래와 같이 참조하고 있는 revision 을 관리해주며 update 통해서 최신 소스를 받아 올수 있습니다.
{
  "version": 0,
  "dependencies": [
    {
      "importpath": "github.com/Buzzvil/go-env",
      "repository": "https://github.com/Buzzvil/go-env",
      "vcs": "git",
      "revision": "2d8489d40184a12c4d09d09ce1ff717e5dbb0745",
      "branch": "master",
      "notests": true
    },
....

Design pattern

Go 언어에서는 package level cycling dependency 를 허용하지 않아서 좀더 명확한 구조를 만들기 좋았습니다. 예를들어 Service 에서는 Controller 를 참조할수 없고 Model 에서는 Controller / Service / DTO 등을 참조할수 없도록 강제했습니다. 모든 API 요청은 Route 를 통해 Controller 에게 전달되고 이 때 생성된 DTO (Data transfer object) 들을 Controller 가 직접 혹은 Service layer 에서 처리하도록 하였고 DB 에 접근할 때는 모델을 통해 혹은 직접 접근하도록 했지만 추후 구조가 복잡해지면 DB 쿼리 등을 담당하는 DAO (Data access object) 를 도입할 계획입니다

Libraries

요소

이름

선택 이유

Network

Gin

Web 서버이다 보니 네트워크 성능을 최우선으로 고려, 벤치마크 표를 보고 이 라이브러리를 선택

Redis & cache

go-redis

역시 성능을 가장 중요한 지표로 보고 이 라이브러리 선택

Mysql

Gorm

ORM 없이는 개발하기 힘든 시대이죠. 여러 Database를 지원하고 ORM 중에서도 method chaining 을 사용하는 Gorm 을 선택

Dynamo

guregu dynamo

AWS에서 제공하는 Dynamo 패키지를 그대로 사용하면 코드 양이 너무 많아지고 역시 method chaining 을 지원해서 선택

Environment variables

caarlos0 env

Go 에서는 tag 를 이용하면 좀더 코드를 간결하고 읽기 쉽게 사용할수 있는데 이 라이브러리가 환경변수를 읽어오기 쉽도록 해줌

Redis cache

func SetCache(key string, obj interface{}, expiration time.Duration) error {
  err := getCodec().Set(&cache.Item{
    Key:        key,
    Object:     obj,
    Expiration: expiration,
  })
  return err
}

func GetCache(key string, obj interface{}) error {
  return getCodec().Get(key, obj)
}

Mysql

var config model.DeviceContentConfig
env.GetDatabase().Where(&model.DeviceContentConfig{DeviceId: deviceId}).FirstOrInit(&config)

Dynamo

if err := env.GetDynamoDb().Table(env.Config.DynamoTableProfile).Get(keyId, deviceId).All(&profiles); err == nil && len(profiles) > 0 {
  ...
}

Environment variables

var (
  Config     = ServerConfigStruct{}
  onceConfig sync.Once
)

type (
  ServerConfigStruct struct {
    ServerEnv  string `env:"SERVER_ENV"`
    LogLevel   string
....
  }
)

func LoadServerConfig(configDir string) {
  onceConfig.Do(func() {//최초 한번반 호출되도록
    env.Parse(&Config)
  }
}

Unit test

환경 구성

Test 환경에는 Redis / Mysql / Elastic search 등에 대한 independent / isolated 된 환경이 필요해서 이를 위해 docker 환경을 따로 구성하였습니다. Test case 작성은 아래와 같이 package 를 분리해서 작성했습니다.

package buzzscreen_test

var ts *httptest.Server

func TestMain(m *testing.M) {
  ts = tests.GetTestServer(m)
  // 환경 시작
  tearDownElasticSearch := tests.SetupElasticSearch()
  tearDownDatabase := tests.SetupDatabase()

  code := m.Run()    // 여기서 작성한 TestCase 들 실행
  // 환경 종료
  tearDownDatabase()
  tearDownElasticSearch()
  ts.Close()

  os.Exit(code)
}

Mock server는 은 http.RoundTripper interface 를 구현해서 http.Client 의 Transport 멤버로 설정해서 구현했습니다. 아래는 Test case 작성 예제입니다.

httpClient := network.DefaultHttpClient
mockServer := mock.NewTargetServer(network.GetHost(MockServerUrl))
.AddResponseHandler(&mock.ResponseHandler{
  WriteToBody: func() []byte {
    return []byte(mockRes)
  },
  Path:   "/path",
  Method: http.MethodGet,
})
clientPatcher := mock.PatchClient(httpClient, mockServer)
defer clientPatcher.RemovePatch()

Unit test 관련해서는 내용이 방대해서 추후 다른 포스트를 통해 자세히 소개하도록 하겠습니다.

Infra

API 요청 분할

AWS Application load balancer

여러 API 중에서 할당 API 를 제외한 요청은 기존의 Django 서버로 요청을 보내고 할당요청에 대해서만 Go서버로 요청을 보내도록 구현하기 위해 먼저 시도 했던 것은 AWS Application load balancer (이후 ALB) 였습니다. ALB 의 특징이 path 로 요청을 구별해서 처리할수 있었기 때문에 Allocation API 만 Go 서버 로 요청이 가도록 구현했습니다.

출처: Amazon Devops Blog, Introducing Application Load Balancer

하지만 이렇게 오랫동안 서비스 하지 못했는데 그 이유는 서버 구성이 하나 더 늘어나고 앞단에 ALB 까지 추가되다 보니 이를 관리하는데 추가 리소스가 들어가게 되어서 어떻게 하면 이러한 비용을 줄일수 있을까 고민하게 되었습니다.  

Using docker & nginx

Go로 작성된 서버가 독립적인 Micro service 냐 아니면 Django 서버에서 특정 API 를 독립시켜 성능을 강화한 모듈이냐 의 정체성을 두고 생각해봤을때 후자가 조금더 적합하다보니 Go / Django 서버는 한 묶음으로 관리하는 것이 명확했습니다. Docker 를 도입하면서 nginx container 가 proxy 역할을 하고 path를 보고 Go container / Django container 로 요청을 보내는 구성을 가지게 되었습니다.

글을 마치며

시작은 미약하였으나 끝은 창대하리라

하나의 API를 이전했음에도 불구하고 Allocation API 에 대해서는 약 1/3, 서버 Instance 비용은 1/2.5 수준으로 감소했습니다.

설명: 기존 4개의 Django 인스턴스의 CPU 사용률이 모두 13% 정도 감소, Go 인스턴스의 CPU 사용율은 17% 정도   17 / (13 * 4)  ≒ 1 / 3

충분히 만족할만한 성과가 나와서 그 뒤로 몇가지 API도 Go 로 옮겼고 새로 작성하는 API 는 Go 환경 안에서 직접 구현하는 중입니다. 처음에는 호출이 많은 하나의 API 를 다른 언어로 포팅하기 위해 시작한 작업이었는데 Container 기술을 도입하는 등 서버 Infra 까지 변경하면서 상당히 큰 작업이 뒤따르게 되었습니다. 하지만 이 작업을 하면서 많은 동료들의 도움과 조언이 있었고 결국 완성할수 있었습니다. 이렇게 실험적인 도전을 성공 할수 있는 환경에 여러분을 초대하고 싶습니다! Go언어에 대한 문의나 좋은 의견도 환영합니다.

%3Cscript%3E%0A%20%20fbq%28%27track%27%2C%20%27ViewContent_Tech%27%29%3B%0A%3C%2Fscript%3E%0A%3Cscript%3E%0A%20%20var%20button%20%3D%20document.getElementById%28%27workable%27%29%3B%0A%20%20button.addEventListener%28%0A%20%20%20%20%27click%27%2C%0A%20%20%20%20function%28%29%20%7B%0A%20%20%20%20%20%20fbq%28%27trackCustom%27%2C%20%273rd_stage_conversion%27%2C%20%7B%0A%20%20%20%20%20%20%20%20content_name%3A%20%27Workable%27%2C%0A%20%20%20%20%20%20%7D%29%3B%0A%20%20%20%20%20%20fbq%28%27trackCustom%27%2C%20%27Click_Workable%27%2C%20%7B%0A%20%20%20%20%20%20%20%20content_name%3A%20%27%27%2C%0A%20%20%20%20%20%20%7D%29%3B%0A%20%20%20%20%7D%2C%0A%20%20%20%20false%0A%20%20%29%3B%0A%3C%2Fscript%3E%0A

You May Also Like

post-thumb
  • 27 Sep, 2017

아마존 에코를 활용한 음성 인식 에어컨 제어

에어컨 제어를 위한 회로 설계 및 LIRC 사용 이 포스팅은 하드웨어 대한 지식이 부족한 독자도 이해할 수 있도록 하는 것을 목표로 하여 작성되었습니다. 국민학교 시절에 문방구에서 팔던 사이렌, 라디오 조립 키트를 기억하시나요? 어렸을적 과학시간에 배운 꼬마전구 회 …

Read Article
post-thumb
  • 08 Aug, 2017

안드로이드 파편화(Fragmentation)에 대하여

1. 글을 시작하며 저는 버즈빌에서 안드로이드 개발을 하고 있는 초보 개발자 Evan입니다. 버즈빌에서 개발을 시작한지 이제 1년정도 되었는데요, 작년 이맘때와 비교하면 회사의 훌륭한 개발자분들과 같이 일하면서 스스로 많이 성장했구나 라는 생각을 하면서도, 한편으로는 …

Read Article