• 서론

다중 서버에서 측정한 데이터 전송 시간이 음수로 나타나는 문제가 발생하였다.

전송 시간은 [수신 시각 - 송신 시각]으로 계산하였는데, 이 값이 음수로 나타난 것은 서버들의 시간 동기화가 제대로 이루어져 있지 않기 때문으로 보인다.

이를 해결하기 위해, NTP(Network Time Protocol)로 서버들의 시간을 동기화하자.



  • 본론

(1) 전송 시간 측정 프로그램


서버 간의 데이터 전송 시간을 측정하기 위해, 다음과 같이 파이썬으로 구현된 소켓 통신 프로그램을 사용해보자.

소켓 통신 프로그램은 "파이썬에서 간단한 소켓 통신 예제"를 수정하였다. 


먼저 서버 프로그램이다.

  1. 5번 라인에서 인자로 받은 서버의 ip 값을 저장한다.
  2. 6~9번 라인에서 서버 소켓을 열고 대기한다.
  3. 10번 라인에서 클라이언트로부터 데이터를 입력받는다. 데이터는 클라이언트에서 측정한 송신 시각이다.
  4. 11번 라인에서 수신 시각을 측정하고, 전달받은 송신 시각과의 차이를 출력한다. 각 시각은 마이크로초 단위이며, 자세한 내용은 "파이썬에서 Microsecond(마이크로초) 단위까지 시간 출력하기"를 참고한다.
1
2
3
4
5
6
7
8
9
10
11
import socket
import sys
from datetime import datetime
 
ip = sys.argv[1]
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind((ip, 12345))
server_socket.listen(0)
client_socket, addr = server_socket.accept()
data = client_socket.recv(65535)
print(int(datetime.utcnow().strftime('%S%f')) - int(data))
cs


다음은 클라이언트 프로그램이다.

  1. 5번 라인에서 인자로 받은 서버의 ip 값을 저장한다.
  2. 6~7번 라인에서 서버와 연결을 요청한다.
  3. 8번 라인에서 서버에게 송신 시각을 측정하여 전달한다.
1
2
3
4
5
6
7
8
import socket
import sys
from datetime import datetime
 
ip = sys.argv[1]
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((ip, 12345))
sock.send(datetime.utcnow().strftime('%S%f'))
cs


이제 두 프로그램을 실행하여 전송 시간을 측정해보자.

두 개의 쉘을 열고, 먼저 서버 프로그램을 실행한다.


1
2
[user@vm1~]$ python server.py vm1
 
cs


서버 프로그램이 실행된 후, 클라이언트로부터 연결을 대기하고 있다.

이제 다른 쉘에서 클라이언트 프로그램을 실행한다.


1
2
[user@vm2 ~]$ python client.py vm1
[user@vm2 ~]$
cs


클라이언트 프로그램을 실행하면, 메시지 없이 프로그램이 바로 종료된다.

다시 서버 프로그램을 실행한 쉘로 돌아가보면, 숫자 값이 출력되어 있을 것이다.

이 숫자가 클라이언트에서 서버로 데이터를 송신할 때 걸린 전송 시간(단위: 마이크로 초)이 된다.

아래 예제에서는 1015 마이크로 초(1015 * 10^-6 초)로 측정되었다.


1
2
3
[user@vm1 ~]$ python server.py vm1
1015
[user@vm1 ~]$
cs



(2) 동기화가 되지 않은 다중 서버


어떤 시간 동기화 작업을 하지 않은 경우, 운영 체제를 설치하면서 초기화했던 시간 덕분에 초 단위까지는 거의 비슷할 것이다.

하지만 앞서 (1)에서 설명한 전송 시간 측정 프로그램으로 다중 서버간의 전송 시간을 측정해보면 그 문제를 확인할 수 있다.

예를 들어, 동기화가 되지 않은 세 대의 CentOS 가상머신에서 서로 간의 전송 시간을 측정해보니 다음과 같은 결과가 나타났다.


<동기화가 되지 않은 다중 서버에서 데이터 전송 시간>


측정 결과를 보면, 전송 시간들의 편차가 매우 클뿐만 아니라 음수가 나타난 것을 알 수 있다.

즉, 송신 시각이 수신 시각보다 더 큰 것이다.

따라서 각 서버들의 시간을 반드시 동기화해야 한다.



(3) 공용 NTP 서버 time.bora.net를 이용한 동기화


시간 동기화에 가장 많이 쓰이는 프로토콜인 NTP로 동기화해보자.

동기화 기준으로는 공용 NTP 서버인 time.bora.net을 사용한다.

각 서버에서 다음 명령을 실행하고, 동기화가 잘 이루어지도록 10분을 대기하였다.


1
sudo ntpdate –b time.bora.net
cs


10분이 지난 후, (2)와 같이 (1)의 전송 시간 측정 프로그램으로 서버간의 전송 시간을 측정해보았다.

측정 결과는 아래와 같다.


<공용 NTP 서버를 통한 시간 동기화>


측정 결과를 보면, (2)의 동기화되지 않은 경우에 비해 전송 시간들의 편차가 많이 줄었다.

하지만 여전히 결과에 음수가 포함되어 있다.

이는, 공용 NTP 서버와의 통신에 필요한 hop의 수가 많다보니 서버마다 오차가 발생하기 때문이다.

결과의 음수들은 작게는 수십, 크게는 수백으로 나타났는데 이는 밀리 초 이하의 오차라는 것을 추정할 수 있다.



(4) 로컬 NTP 서버를 이용한 동기화


앞서 (3)에서의 동기화에 오차가 발생한 이유가 공용 서버와의 통신 hop이 많기 때문이라고 하였다.

이를 해결하기 위해, NTP 서버를 공용 서버가 아닌 로컬 서버로 지정한다.

현재 서버는 vm1, vm2, vm3가 있으며, 이 중 vm1을 로컬 NTP 서버로 지정하였다.

로컬 NTP 서버를 통한 동기화 방법은 다음 글을 참고한다.

Linux에서 Local NTP를 이용한 다중 서버의 시간 동기화하기


로컬 NTP 서버로 동기화한 후에 (1)의 전송 시간 측정 프로그램으로 서버간의 전송 시간을 측정해보았다.

측정 결과는 아래와 같다.


<로컬 NTP 서버를 통한 동기화>

측정 결과를 보면, 전송 시간의 편차가 크게 줄었을 뿐만 아니라 음수도 나타나지 않음을 알 수 있다.

이렇듯, 다중 서버에서 시간을 동기화할 때에는 공용 NTP 서버보다 로컬 NTP 서버를 사용한다면 오차를 최소화할 수 있다.




  • 결론

다중 서버의 시간 동기화를 위해 로컬 NTP 서버를 사용해보았다.

그 결과, 공용 NTP 서버에 비해 편차도 적으며 음수도 발생하지 않음을 확인하였다.




  • 참고 사이트

How to keep the time on multiple servers in sync, https://serverfault.com/questions/625072/how-to-keep-the-time-on-multiple-servers-in-sync/625696

  • 서론

다수의 서버를 사용하는 실시간 분산 시스템(Apache Kafka, Apache Storm 등)에서 시간 성능을 측정할 일이 생겼다.

성능 측정을 위해 A서버가 B서버에게 메시지를 전송할 때, A서버에서 송신시간을 측정하고 B서버에서 수신시간을 측정하여 {수신시간 - 송신시간}으로 전송시간을 계산하려고 하였다.

이때, 실시간 시스템이 워낙 빠르게 데이터를 주고 받으니 Java의 System.nanoTime()으로 송신시간과 수신시간을 측정했다.

그런데 측정된 시간으로 전송시간({수신시간 - 송신시간})을 계산해보니 음수가 나오는 문제가 발생했다.

즉, 이는 송신시간이 수신시간보다 더 느리다는 뜻이다.

어디서 이런 문제가 발생했을까?



  • 본론

혹시 다른 프로세스에서 System.nanoTime()을 출력하면 다른 값이 나오는걸까?

먼저 아래와 같이 10ms(0.01초)마다 System.nanoTime()을 출력하는 간단한 프로그램을 작성해보았다.


1
2
3
4
5
6
7
8
9
10
public class App 
{
    public static void main( String[] args ) throws InterruptedException
    {
        while (true) {
            System.out.println( args[0+ ": " + System.nanoTime() );
            Thread.sleep(10);
        }
    }
}
cs


이 프로그램을 두 개의 프로세스로 동작시켰더니 아래와 같은 결과가 나왔다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[admin@vm~]$ java -cp test-nanosecond.jar App test01 & java -cp test-nanosecond.jar App test02 &
[13384
[23385
test01: 9990558350836
test02: 9990562699619
test01: 9990568550330
test02: 9990572910761
test01: 9990578835578
test02: 9990583035689
test01: 9990588908712
test02: 9990593405835
test01: 9990599289354
test02: 9990604186813
test01: 9990610090038
test02: 9990614317801
test01: 9990621349641
test02: 9990624723916
test01: 9990631595658
test02: 9990635501565
test01: 9990641702175
test02: 9990645620668
test01: 9990652082446
test02: 9990655972680
test01: 9990662856546
test02: 9990666745119
test01: 9990672977516
test02: 9990676842676
(이하 생략)
cs

실험 결과, test01 프로세스와 test02 프로세스가 출력하는 System.nanoTime()은 서로 반복적으로 교차하지만 선형적으로 증가함을 확인하였다.

즉, 서버 내에서 동작하는 서로 다른 프로세스에서는 System.nanoTime()이 꼬이지 않고 정상적으로 측정된다.


이제 두 개의 서버에서 위의 프로세스를 하나씩 동작시켜보자.

실험은 MobaXterm의 Multi-execution mode를 사용하여 동시에 실행하였다.

실험 결과는 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[admin@vm1 ~]$ java -cp test-nanosecond.jar App test01
test01: 10471422550227
test01: 10471433277287
test01: 10471443430335
test01: 10471453691607
test01: 10471463805147
test01: 10471474252026
test01: 10471484969633
test01: 10471495117322
test01: 10471505516197
test01: 10471516402940
test01: 10471527116008
test01: 10471537546092
test01: 10471547990703
cs


1
2
3
4
5
6
7
8
9
10
11
12
13
14
[admin@vm2 ~]$ java -cp test-nanosecond.jar App test02
test02: 10467594802870
test02: 10467605744154
test02: 10467616455625
test02: 10467627232689
test02: 10467637970750
test02: 10467648788369
test02: 10467659470987
test02: 10467670270827
test02: 10467681010944
test02: 10467691769703
test02: 10467702478691
test02: 10467713257345
test02: 10467724012165
cs


실험 결과를 보면, 쉘의 입출력 시간과 터미널이 결과를 보여주는 시간을 고려하더라도 큰 차이가 있음을 알 수 있다.

예를 들어, 각 2번 라인을 보면 test01이 test02에 비해 3,827,747,357이 더 크다.

System.nanoTime()은 나노 초(10^-9 초)를 측정하므로, test01이 test02에 비해 약 3.8초 정도 느린 시간을 측정한다는 것이다.

이로써 System.nanoTime()을 서버 간의 시간 성능 측정에 사용할 경우 수 초 정도의 오차가 발생함을 확인하였다.


이러한 문제의 원인은 System 클래스의 Java doc을 통해 확인할 수 있었다.

Java doc의 nanoTime() 메서드 설명을 보면 아래의 설명이 있었다.

Returns the current value of the running Java Virtual Machine's high-resolution time source, in nanoseconds.

(중략)

The same origin is used by all invocations of this method in an instance of a Java virtual machine; other virtual machine instances are likely to use a different origin.

즉, System.nanoTime() 메서드는 JVM을 기준으로 나노초를 측정하며, 다른 JVM에서는 기준이 다르다는 뜻이다.

이러한 이유로 서로 다른 서버에서 측정한 나노초는 오차가 발생하는 것이다.


  • 결론

Java의 System.nanoTime()은 같은 서버에서 서로 다른 프로세스로 동작시킬 경우에는 결과가 꼬이는 문제가 발생하지 않는다.

하지만 서로 다른 서버에서 시간 성능 측정으로 사용할 경우에는 수 초 정도의 오차가 발생할 수 있다.

따라서 서로 다른 서버에서는 단위가 조금 크더라도 System.currentTimeMillis()를 사용해야 한다.

System.currentTimeMillis() 함수는 운영체제로부터 시간을 가져오므로 서버들의 시간 동기화를 잘 맞춰준다면 초 단위의 오차를 없앨 수 있다.

(시간 동기화 후에 추가로 실험해보니 System.currentTimeMillis()은 초 단위의 오차가 발생하지 않았다.)


  • 실험 환경

OS: CentOS 7

JVM: 1.8

+ Recent posts