[Tech Blog] Go 서버 개발하기

 

버즈빌 / 19. 04. 11. 오후 3:37 / 조회수 : 2110

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 하다보니 자유도를 제한해서 코드를 보기 쉽게 하는 것을 선호하는 저의 성격과도 잘 맞는 언어였습니다.
 
 
 
 
 

서버 개발 환경

 

 

 

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

 
                 
요소이름선택 이유
NetworkGinWeb 서버이다 보니 네트워크 성능을 최우선으로 고려, 벤치마크 표를 보고 이 라이브러리를 선택
Redis & cachego-redis역시 성능을 가장 중요한 지표로 보고 이 라이브러리 선택
MysqlGormORM 없이는 개발하기 힘든 시대이죠. 여러 Database를 지원하고 ORM 중에서도 method chaining 을 사용하는 Gorm 을 선택
Dynamoguregu dynamoAWS에서 제공하는 Dynamo 패키지를 그대로 사용하면 코드 양이 너무 많아지고 역시 method chaining 을 지원해서 선택
Environment variablescaarlos0 envGo 에서는 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 서버 로 요청이 가도록 구현했습니다.

 

  

 

 

하지만 이렇게 오랫동안 서비스 하지 못했는데 그 이유는 서버 구성이 하나 더 늘어나고 앞단에 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언어에 대한 문의나 좋은 의견도 환영합니다.


기업문화 엿볼 때, 더팀스

로그인

/