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

버즈빌, 아마도 당신이 원하던 회사!

지원하기