Yunseok's Dev Blog

배운 것을 적는 블로그입니다.

Express Production Best Practices

Performance

Use gzip compression

  • Gzip 압축은 응답 바디의 사이즈를 줄일 수 있어서 웹 앱의 속도를 높일 수 있습니다.
  • compression 미들웨어는 Gzip압축을 지원하는 미들웨어 입니다.
var compression = require('compression')
var express = require('express')
var app = express()
app.use(compression())
  • 트레픽이 많은 웹사이트의 경우 더 좋은 방법은 reverse proxy 레벨에서 압축을 사용하는 것입니다.(Use a reverse proxy)
  • 그럴 경우 미들웨어에서 압축을 사용할 필요가 없습니다.
  • Nginx에서 Gzip압축을 사용하려면 다음을 참고하세요: Module ngx_http_gzip_module

Don’t use synchronous functions

  • 동기함수, 메소드는 일이 끝날 때 까지 프로세스를 묶어놓습니다. 동기함수로 하나의 요청은 밀리초, 마이크로초에 처리가 끝날 수 있지만 높은 트래픽 웹사이트에서는 앱의 성능을 저하시킵니다.
  • Node가 동기, 비동기버전의 함수들을 지원하더라도 항상 비동기함수를 사용해야합니다. 유일하기 동기함수를 사용할 수 있는 곳은 처음 서버가 시작될 때 뿐입니다.
  • 만약 Node.js 4.0이상을 쓰고 io.js 2.1.0이상을 쓰고 있다면 --trace-sync-io플래그를 사용하면 앱이 동기API를 호출하는 것을 확인할 수 있습니다.

Do logging correctly

  • 일반적으로 로깅을 하는 이유는 디버깅의 목적과 앱의 활동을 기록하는것입니다.
  • 개발할 때 console.log()console.error()를 사용합니다. 하지만 이 함수들은 터미널 혹은 파일로 출력할 때 동기 함수들입니다. 그래서 배포할 때는 적합하지 않습니다.

For debugging

  • 디버깅을 목적으로 한다면 console.log()보다는 debug를 사용하는 것이 좋습니다. 이 모듈은 DEBUG환경 변수를 사용하고 앱이 비동기적으로 동작하도록 해줍니다.

For app activity

  • 앱활동을 기록하려면 console.log대신 로깅 라이브러리를 사용하는 것이 좋습니다.

Handle execeptions properly

에외를 처리하지 않고 적적한 조치를 취하지 않으면 Express앱은 다운되고 종료됩니다. 따라서 예외들을 적절히 처리해야합니다. Node/Express는 두 가지 방식으로 에러를 처리합니다.

  • Error-first callbacks
  • Propagating errors in middleware

Node는 비동기 함수에서 첫 번째 인자로 에러를 반환하고 나머지 반환할 값들을 반환합니다. 만약 에러가 발생하지 않는다면 null을 첫 번째 인자로 반한합니다. callback함수는 반드시 error-frist callback컨벤션을 지켜야합니다. Express에서는 next()함수를 사용하여 에러를 전파하는 것이 best practice입니다.

에러처리에 대한 자세한 내용들은 다음을 참고하세요.

What not to do

당신이하지 말아야 할 일 중 하나는 uncaughtException 이벤트를 수신하는 것입니다. 예외가 발생하면 예외가 이벤트 루프로 되돌아갑니다. uncaughtException을 event listener에 등록하는 것은 프로세스의 기본행동을 변경하게 됩니다. 그러면 프로세스는 예외가 발생해도 계속 실행하게 됩니다. 언듯 듣기에는 앱이 멈추는 것을 방지하기 떄문에 좋아 보이지만 예상치못한 에러가 발생했을 때 프로그램이 계속 실행되는 것은 굉장히 위험한 일입니다.

Use try-catch

Try-catch는 동기 코드에서 예외를 catch하는 데 사용할 수있는 JavaScript 언어 구문입니다.

app.get('/search', function (req, res) {
  // Simulating async operation
  setImmediate(function () {
    var jsonStr = req.query.params
    try {
      var jsonObj = JSON.parse(jsonStr)
      res.send('Success')
    } catch (e) {
      res.status(400).send('Invalid JSON string')
    }
  })
})

하지만 Try-catch는 동기코드에서만 동작합니다. Node는 주로 비동기적으로 프로그램을 작성하므로 많은 예외를 처리하지 못합니다.

Use promises

promise들은 어떤 예외든 catch(next)로 처리합니다.

app.get('/', function (req, res, next) {
  // do some sync stuff
  queryDb()
    .then(function (data) {
      // handle data
      return makeCsv(data)
    })
    .then(function (csv) {
      // handle csv
    })
    .catch(next)
})

app.use(function (err, req, res, next) {
  // handle error
})

모든 동기, 비동기에러들이 에러미들웨어로 전파됩니다. 하지만 두가지 주의해야 할 것이 있습니다.

  • 모든 비동기 코드들은 promise를 맄턴해야합니다. 특정한 라이브러리가 promise를 반환하지 않는다면 promise로 변경해야합니다.
  • Event emiiter들은 여전히 처리되지않은 예외들이 있습니다. 따라서 에러 이벤트를 적절히 처리하고 있는지 확인해야합니다.
const wrap = fn => (...args) => fn(...args).catch(args[2])

app.get('/', wrap(async (req, res, next) => {
  let company = await getCompanyById(req.query.id)
  let stream = getLogoStreamById(company.id)
  stream.on('error', next).pipe(res)
}))

wrap()함수는 reject된 promise들을 처리하고 next()함수를 호출합니다.

Set NODE_ENV to ‘production’

NODE_ENV환경변수는 어플리케이션이 동작하는 환경을 나타냅니다. 일반적으로 developmentenvironment가 있습니다. 성능을 올리는데 가장 간단한 것은 NODE_ENVproduction으로 설정하는 것입니다. Express에서 NODE_ENVproduction으로 할 경우

  • view템플릿들을 캐싱합니다.
  • CSS extension로부터 생성된 CSS파일들을 캐싱합니다.
  • 에러메세시지들을 자세하게 표시하지 않습니다.

Test indicate에서 테스트결과를 확인할 수 있습니다. 환경에 따라 다른 코드를 작성하고 싶다면 process.env.NODE_ENV를 확인하여 작성할 수 있습니다.

Ensure your app automatically restarts

production에서는 어플리케이션이 종료되는 것을 원치 않습니다. 만약 앱이 멈추면 앱은 재시작해야 합니다.

  • 프로세스 매니저를 사용하여 앱이 종료되었을 때 재시작합니다.
  • OS에 있는 초기화 시스템으로 프로세스 매니저를 재시작합니다.

노드 애플리케이션은 처리되지 않은 예외가 있을 때 멈춥니다. 예외를 모두 처리하는 것이 더 중요하지만 만약 앱이 종료되었을 때 다시 시작하는 것이 필요합니다.

Use process manager

개발할 때는 CLI에서 서버를 실행시킵니다. 하지만 production에서 이렇게 하면 안됩니다. 앱이 멈추면 다시 시작할 떄 까지 서비스가 멈추게 됩니다. 앱이 멈추었을 때 다시 시작하도록 프로세스 매니저를 사용하는 것이 좋습니다. 프로세스 매니저는 애플리케이션들을 위한 컨테이너로 배포와 높은 가용성, 그리고 애플리케이션을 관리할 수 있도록 도와줍니다. 프로세스매니저는 단순히 앱을 재시작 하는 기능 뿐만 아니라 다음같은 기능들도 제공합니다.

  • 자원소비와 실행 성능의 정보를 볼 수 있습니다.
  • 성능향상을 위해 동적으로 설정을 변경할 수 있습니다.
  • 클러스터링을 관리합니다.

가장 유명한 프로세스 매니저들은 다음과 같습니다.

Use an init system

서버가 재시작 될 때 애플리케이션 또한 재시작되어야 합니다. 시스템들은 여러가지 이유로 다운될 수 있기 때문에 서버가 재시작 될 때 애플리케이션도 또한 재시작 되어야 합니다. systemdUpstart가 가장많이 사용됩니다.

  • 프로세스 매니저에서 앱을 실행시키고 init 시스템과 함께 프로세스 매니저를 설치해야 합니다. 앱이 충돌로 중단이 되었을 때 프로세스 매니저가 앱을 재시작하게 되고 OS가 재시작될 대 init 시스템이 프로세스 매니저를 재시작 하게 됩니다.

Systemd

Systemd는 리눅스 시스템의 서비스 매니저입니다. 대부분의 리눅스 배포판에서 기본으로 탑재되어 있습니다. systemd서비스 설정파일은 unit file이고 .service라는 이름으로 작성합니다.

[Unit]
Description=Awesome Express App

[Service]
Type=simple
ExecStart=/usr/local/bin/node /projects/myapp/index.js
WorkingDirectory=/projects/myapp

User=nobody
Group=nogroup

# Environment variables:
Environment=NODE_ENV=production

# Allow many incoming connections
LimitNOFILE=infinity

# Allow core dumps for debugging
LimitCORE=infinity

StandardInput=null
StandardOutput=syslog
StandardError=syslog
Restart=always

[Install]
WantedBy=multi-user.target

위의 예제는 직접적으로 node를 실행시킵니다.

StringLoop PM as a systemd service

쉽게 StringLoop PM을 systemd service로 설치할 수 있는데 그러면 서버가 재시작 될 때 자등으로 StringLoop PM을 실행할 것입니다. StringLoop PM을 systemd service로 설치하려면 다음과 같이 입력하면 됩니다.

$ sudo sl-pm-install --systemd

그리고 서비스를 재시작할 때는 다음과 같이 입력합니다.

$ sudo /usr/bin/systemctl start strong-pm

Upstart

Upstart는 만은 리눅스 배포판에서 사용가능한 시스템 도구 입니다. Express혹은 PM을 서비스로서 설정할 수 있고 Upstart가 서버가 재시작 될 때 Express혹은 PM을 재시작 할 것입니다.

Upstart서비스는 job설정파일로 정의되는데 .conf로 끝나는 이름으로 작성됩니다. 밑의 예는 설정파일 예제입니다. myapp.conf/etc/init/에 만들고 다음과 같이 설정 할 수 있습니다.

# When to start the process
start on runlevel [2345]

# When to stop the process
stop on runlevel [016]

# Increase file descriptor limit to be able to handle more requests
limit nofile 50000 50000

# Use production mode
env NODE_ENV=production

# Run as www-data
setuid www-data
setgid www-data

# Run from inside the app dir
chdir /projects/myapp

# The process to start
exec /usr/local/bin/node /projects/myapp/index.js

# Restart the process if it is down
respawn

# Limit restart attempt to 10 times within 10 seconds
respawn limit 10 10

StrongLoop PM as an Upstart service

StrongLoop PMUpstart서비스로서 쉽게 설치할 수 있습니다. 서버가 재시작 될 때 자동으로 PM이 재시작 됩니다.

$ sudo sl-pm-install

설치 후 서비스를 실행시켜줍니다.

$ sudo /sbin/initctl start strong-pm

Run your app in cluster

멀티코어 시스템에서 Node 애플리케이션의 프로세스들의 클러스터를 실행시켜 성능을 향상시킬 수있습니다. 클러스터는 앱의 인스턴스를 여러개 실행시켜 각 CPU core마다 하나의 인스턴스를 실행시켜 인스턴스의 부하를 줄일 수 있습니다.

Sources