사전 RPC
프로토콜

RPC

gabury1

다른 프로세스에 있는 프로시저를 자기 코드 안의 함수처럼 부르는 규칙입니다. 부르는 쪽이 인자를 담은 메시지를 하나 보내면 결과를 담은 메시지가 하나 돌아옵니다. 코드에서 보이는 모양은 함수 호출이지만 그 사이에는 네트워크가 있습니다.

상세

RPC(Remote Procedure Call, 원격 프로시저 호출)는 호출자가 다른 프로세스의 프로시저를 부르고 결과를 돌려받는 모델을 메시지 규격으로 적은 것입니다. 이 항목이 필드 이름과 값을 대는 근거는 IETF 표준화 문서 RFC 5531 이 정의하는 ONC RPC(Open Network Computing RPC) 판입니다. 다른 계열은 필드 이름과 값이 다릅니다.

명세는 이 모델을 로컬 프로시저 호출에 견줍니다. 로컬에서는 호출자가 프로시저의 인자를 정해진 자리에 놓습니다. 그리고 제어를 프로시저로 넘긴 뒤 언젠가 제어를 되찾습니다. 그 시점에 정해진 자리에서 결과를 꺼내고 실행을 이어 갑니다.

원격도 비슷합니다. 하나의 제어 흐름이 논리적으로 두 프로세스를 감고 지나갑니다. 호출자의 프로세스와 서버의 프로세스입니다. 호출자는 먼저 호출 메시지를 서버 프로세스로 보내고 응답 메시지를 기다립니다. 이때 호출자는 블록됩니다. 호출 메시지에는 프로시저의 파라미터가 들어 있습니다. 응답 메시지에는 프로시저의 결과가 들어 있습니다. 응답 메시지를 받으면 결과를 꺼내고 호출자의 실행이 재개됩니다. 서버 쪽 프로세스는 호출 메시지가 올 때까지 잠들어 있습니다. 하나가 도착하면 파라미터를 꺼내 결과를 계산하고 응답 메시지를 보낸 뒤 다음 호출 메시지를 기다립니다.

이 모델에서는 두 프로세스 중 한쪽만 어느 시점에나 활성입니다. 다만 명세는 이것을 예로 든 것일 뿐이라고 못 박습니다. ONC RPC 프로토콜은 구현하는 동시성 모델에 아무 제한을 두지 않고 다른 방식도 가능합니다. 어떤 구현은 RPC 호출을 비동기로 만들어 클라이언트가 응답을 기다리는 동안 다른 일을 하게 할 수 있습니다. 서버가 들어온 호출마다 별도 태스크를 만들어 원래 서버는 다른 요청을 계속 받게 하는 방식도 있습니다.

무엇을 부를지는 세 개의 번호가 지목합니다. RPC 호출 메시지에는 부호 없는 정수 필드가 셋 있습니다. 원격 프로그램 번호, 원격 프로그램 버전 번호, 원격 프로시저 번호입니다. 이 셋이 부를 프로시저를 유일하게 가리킵니다. 프로그램 번호는 중앙 기관인 IANA(Internet Assigned Numbers Authority, 인터넷 할당 번호 관리 기관)가 배정합니다. 구현자가 프로그램 번호를 받으면 자기 원격 프로그램을 구현할 수 있습니다. 첫 구현은 대개 버전 번호 1 을 달지만 0 이어서는 안 됩니다. 프로토콜이 대개 진화하므로 호출 메시지의 버전 필드가 호출자가 쓰는 프로토콜의 판을 알립니다. 버전 번호 덕에 한 서버 프로세스가 옛 프로토콜과 새 프로토콜을 함께 받아 줍니다. 프로시저 번호는 부를 프로시저를 가리키고, 이 번호들은 그 프로그램의 프로토콜 명세에 적힙니다. 파일 서비스 명세라면 프로시저 번호 5 를 read, 12 를 write 로 적어 둘 수 있습니다. 호출 메시지에는 RPC 메시지 프로토콜 자신의 버전 번호도 들어갑니다. 여기서 기술하는 판에서는 언제나 2 입니다.

용어의 범위도 좁게 정해져 있습니다. 네트워크 서비스는 원격 프로그램 하나 이상의 모음입니다. 원격 프로그램은 원격 프로시저를 하나 이상 구현합니다. 클라이언트와 서버라는 말은 특정 트랜잭션에만 적용됩니다. 같은 하드웨어 개체나 같은 소프트웨어 개체가 때에 따라 두 역할을 다 맡을 수 있습니다. 원격 실행 서비스를 제공하는 프로그램이 네트워크 파일 서비스의 클라이언트일 수도 있습니다.

명세는 원격 호출이 로컬 호출과 다른 자리를 넷으로 짚습니다.

  • 에러 처리. 원격 프로시저 호출을 쓸 때는 원격 서버나 네트워크의 실패를 다뤄야 합니다
  • 전역 변수와 부작용. 서버는 클라이언트의 주소 공간에 접근하지 못합니다. 숨은 인자를 전역 변수로 넘기거나 부작용으로 돌려받을 수 없습니다
  • 성능. 원격 프로시저는 대개 로컬 프로시저 호출보다 한 자릿수 이상 느리게 동작합니다
  • 인증. 원격 프로시저 호출은 보안되지 않은 네트워크 위로 실려 갈 수 있습니다. 그래서 인증이 필요할 수도 있습니다. 인증은 한 개체가 다른 개체 행세를 하는 것을 막습니다

바인딩은 이 명세 밖입니다. 특정 클라이언트를 특정 서비스와 전송 파라미터에 묶는 행위는 RPC 프로토콜 명세의 일부가 아닙니다. 명세는 그 일을 상위 소프트웨어에 맡깁니다. 명세 자신은 RPC 를 네트워크의 점프 서브루틴 명령에 견줍니다. 로더가 그 명령을 쓸모 있게 만들고, 로더 자신도 그 명령을 써서 일을 합니다.

메시지를 바이트로 적는 규칙은 이 명세가 직접 정하지 않습니다. 명세는 RPC 메시지 프로토콜을 XDR(eXternal Data Representation, 외부 데이터 표현) 데이터 기술 언어로 정의합니다. 스텁 · 마샬링 · IDL(Interface Definition Language, 인터페이스 정의 언어)은 각각 따로 볼 이야기입니다. gRPC · JSON-RPC 처럼 이름에 RPC 가 들어간 다른 계열도 있습니다.

교환 순서

한 왕복은 호출 메시지 하나와 응답 메시지 하나입니다. 메시지 종류는 두 가지뿐입니다. CALL 이 0, REPLY 가 1 입니다.

sequenceDiagram
    participant 클라이언트
    participant 서버
    클라이언트->>서버: CALL xid · rpcvers=2 · prog · vers · proc · cred · verf · 인자
    Note over 서버: 잠들어 있다가 CALL 을 받아 파라미터를 꺼내고 결과를 계산
    서버->>클라이언트: REPLY 같은 xid · MSG_ACCEPTED · SUCCESS · 결과

모든 메시지는 트랜잭션 식별자 xid 로 시작합니다. 그 뒤에 팔이 둘인 판별 union 이 옵니다. union 의 판별자는 msg_type 이고, 그 값에 따라 두 메시지 종류 중 하나로 갈립니다. REPLY 메시지의 xid 는 언제나 그 REPLY 를 불러낸 CALL 메시지의 xid 와 같습니다. 명세는 xid 의 쓰임을 좁게 못 박습니다. 클라이언트가 응답 메시지를 호출 메시지에 짝지을 때, 그리고 서버가 재전송을 알아챌 때만 쓰입니다. 서비스 쪽은 이 식별자를 어떤 종류의 시퀀스 번호로도 취급할 수 없습니다.

struct rpc_msg {
   unsigned int xid;
   union switch (msg_type mtype) {
   case CALL:
      call_body cbody;
   case REPLY:
      reply_body rbody;
   } body;
};

호출 쪽 몸통은 call_body 입니다. 버전 2 명세에서 rpcvers 는 2 여야 합니다. prog · vers · proc 세 필드가 원격 프로그램, 그 버전 번호, 그 안에서 부를 프로시저를 지정합니다. 그 뒤에 인증 파라미터 둘이 옵니다. 인증 자격을 담은 cred 와 인증 검증자인 verf 입니다. 검증자의 목적은 자격을 검증하는 것입니다. 이 둘은 역사적으로는 따로지만 언제나 하나의 논리적 개체로 함께 쓰입니다. 인증 파라미터 뒤부터 원격 프로시저의 파라미터가 이어지고, 그 생김새는 프로그램별 프로토콜 명세가 정합니다.

struct call_body {
   unsigned int rpcvers;       /* must be equal to two (2) */
   unsigned int prog;
   unsigned int vers;
   unsigned int proc;
   opaque_auth cred;
   opaque_auth verf;
   /* procedure-specific parameters start here */
};

응답은 두 모양 중 하나입니다. 메시지가 수락됐거나 거절됐거나입니다. 수락이면 MSG_ACCEPTED 이고 accepted_reply 가 실립니다. 그 첫 필드는 서버가 자신을 클라이언트에게 검증시키려고 만든 인증 검증자입니다. 그 뒤에 accept_stat 를 판별자로 삼는 union 이 옵니다. SUCCESS 팔의 내용은 프로그램별 프로토콜이 정합니다. 거절이면 MSG_DENIED 이고 rejected_reply 가 실립니다.

union reply_body switch (reply_stat stat) {
case MSG_ACCEPTED:
   accepted_reply areply;
case MSG_DENIED:
   rejected_reply rreply;
} reply;

예시

PING_PROG

RFC 5531 이 RPC 언어로 적어 둔 서비스 정의 한 벌입니다.

program PING_PROG {
      /*
       * Latest and greatest version
       */
      version PING_VERS_PINGBACK {
         void
         PINGPROC_NULL(void) = 0;
         /*
          * Ping the client, return the round-trip time
          * (in microseconds).  Returns -1 if the operation
          * timed out.
          */
         int
         PINGPROC_PINGBACK(void) = 1;
      } = 2;

      /*
       * Original version
       */
      version PING_VERS_ORIG {
         void
         PINGPROC_NULL(void) = 0;
      } = 1;
   } = 1;

   const PING_VERS = 2;      /* latest version */

맨 끝의 = 1 이 프로그램 번호입니다. 그 안의 = 2 와 = 1 이 버전 번호이고, 프로시저 이름 뒤의 = 0 과 = 1 이 프로시저 번호입니다. 앞의 판 PING_VERS_PINGBACK 에는 프로시저가 둘 있습니다. PINGPROC_NULL 은 인자도 결과도 없지만 클라이언트에서 서버로 갔다 오는 왕복 시간을 재는 데 쓰입니다. 관례상 어떤 RPC 프로토콜에서든 프로시저 0 은 같은 의미를 가져야 하고 어떤 종류의 인증도 요구하지 말아야 합니다. 두 번째 프로시저는 서버가 클라이언트 쪽으로 역방향 핑을 하게 하고 그 동작에 걸린 시간을 마이크로초로 돌려줍니다. 뒤의 판 PING_VERS_ORIG 는 원래 판입니다. 여기에는 PINGPROC_PINGBACK 이 없습니다. 옛 클라이언트 프로그램과의 호환에 쓰입니다.

NFS 버전 3

프로그램 번호는 대역으로 갈라져 있습니다.

0x00000000                Reserved
0x00000001 - 0x1fffffff   To be assigned by IANA
0x20000000 - 0x3fffffff   Defined by local administrator
                          (some blocks assigned here)
0x40000000 - 0x5fffffff   Transient
0x60000000 - 0x7effffff   Reserved
0x7f000000 - 0x7fffffff   Assignment outstanding
0x80000000 - 0xffffffff   Reserved

첫 대역은 IANA 가 관리하고 모든 사이트에서 같아야 합니다. 두 번째 대역은 특정 사이트에만 있는 응용을 위한 자리이고 주로 새 프로그램을 디버깅하는 용도입니다. 세 번째 대역은 프로그램 번호를 동적으로 만들어 내는 응용을 위한 자리입니다.

실제로 배포된 값이 RFC 1813 에 있습니다. NFS(Network File System, 네트워크 파일 시스템) 버전 3 서비스를 부르는 데 필요한 RPC 상수를 십진수로 못 박아 둡니다.

PROGRAM  100003
VERSION  3

JSON-RPC 2.0

성격이 다른 계열의 한 왕복입니다. JSON-RPC 2.0 명세는 예제에서 --> 를 서버로 보낸 데이터, <-- 를 클라이언트로 보낸 데이터로 씁니다.

--> {"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}
<-- {"jsonrpc": "2.0", "result": 19, "id": 1}

부를 대상을 세 개의 번호가 아니라 method 문자열이 가리킵니다. 요청에 붙은 id 가 응답에 그대로 돌아옵니다. 없는 메서드를 부르면 결과 대신 오류가 실립니다.

--> {"jsonrpc": "2.0", "method": "foobar", "id": "1"}
<-- {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "1"}

실패

응답이 오는 실패와 응답이 안 오는 실패가 따로 있습니다. 응답이 오면 먼저 수락과 거절로 갈립니다.

거절은 두 이유뿐입니다. 서버가 호환되는 판의 RPC 프로토콜을 돌리고 있지 않거나, 서버가 호출자의 신원을 거부한 경우입니다.

reject_stat 뜻 응답에 실리는 것
RPC_MISMATCH (0) RPC 버전 번호가 2 가 아니다 서버가 지원하는 최저·최고 RPC 버전 번호
AUTH_ERROR (1) 원격이 호출자를 인증하지 못한다 실패 상태를 담은 auth_stat

auth_stat 는 인증이 왜 실패했는지를 나눕니다. 원격 쪽에서 실패한 값은 AUTH_BADCRED(자격이 잘못됨) · AUTH_REJECTEDCRED(클라이언트가 새 세션을 시작해야 함) · AUTH_BADVERF(검증자가 잘못됨) · AUTH_REJECTEDVERF(검증자가 만료됐거나 재생됨) · AUTH_TOOWEAK(보안상의 이유로 거부) 입니다. 로컬에서 실패한 값은 AUTH_INVALIDRESP(응답 검증자가 엉터리)와 AUTH_FAILED(이유 모름)입니다. RPCSEC_GSS 관련 값으로 RPCSEC_GSS_CREDPROBLEM 과 RPCSEC_GSS_CTXPROBLEM 이 있습니다. 새 인증 방식이 더해지면 상태 코드가 더 필요해질 수도 있고, IANA 가 선착순으로 새 auth_stat 번호를 내줍니다.

수락됐다고 성공은 아닙니다. 수락된 뒤의 상태는 accept_stat 가 나눕니다.

accept_stat 명세가 적은 뜻
SUCCESS (0) RPC 가 성공적으로 실행됐다
PROG_UNAVAIL (1) 원격이 그 프로그램을 내보내지 않았다
PROG_MISMATCH (2) 원격이 그 버전 번호를 받아 줄 수 없다
PROC_UNAVAIL (3) 프로그램이 그 프로시저를 받아 줄 수 없다
GARBAGE_ARGS (4) 프로시저가 파라미터를 디코드할 수 없다
SYSTEM_ERR (5) 메모리 할당 실패 같은 것

돌아오는 몸통의 모양도 값마다 다릅니다. PROG_MISMATCH 팔은 서버가 받아 주는 그 원격 프로그램의 최저·최고 버전 번호를 실어 보냅니다. PROG_UNAVAIL · PROC_UNAVAIL · GARBAGE_ARGS · SYSTEM_ERR 네 팔은 비어 있습니다. 명세는 원인도 한정어를 달아 짚습니다. 요청한 프로시저 번호가 없는 것은 대개 클라이언트 쪽 프로토콜 오류나 프로그래밍 오류입니다. 파라미터가 서버 관점에서 쓰레기로 보이는 것도 대개 클라이언트와 서비스가 프로토콜을 두고 어긋난 탓입니다.

응답이 아예 안 오는 경우는 이 프로토콜이 다루지 않습니다. RPC 는 어떤 종류의 신뢰성도 구현하려 하지 않습니다. UDP(User Datagram Protocol, 사용자 데이터그램 프로토콜) 같은 신뢰성 없는 전송 위에서 돈다면 응용이 자기 타임아웃 · 재전송 · 중복 감지 정책을 스스로 구현해야 합니다. RPC 프로토콜이 그 서비스를 제공하지 않기 때문입니다. TCP(Transmission Control Protocol, 전송 제어 프로토콜) 같은 연결 지향 프로토콜을 쓰더라도 서버 크래시를 다루려면 응용에는 여전히 타임아웃과 재연결이 필요합니다.

보장과 가정

이 프로토콜이 약속하는 범위는 메시지 자체입니다. RPC 프로토콜 정의의 범위는 메시지가 한 프로세스에서 다른 프로세스로 어떻게 건네지는지를 제외하고, 메시지의 명세와 해석만 포함합니다. 그래서 RPC 는 여러 다른 전송 프로토콜 위에 구현될 수 있습니다.

가정은 그 아래 전송 계층입니다. 클라이언트와 서버는 전송 프로토콜 선택에 서로 합의해야 합니다. 응용은 이 문서가 정하지 않은 인터페이스를 통해 전송 계층에 관한 정보를 얻고 싶어질 수도 있습니다. 전송 프로토콜이 RPC 메시지의 최대 크기에 제한을 걸 수도 있고, TCP 처럼 스트림 지향이라 크기 제한이 없을 수도 있습니다. 명세는 응용이 아래 전송 프로토콜의 종류를 알아야 할 수도 있다는 점을 짚습니다. 신뢰성 있는 전송 위에서 돈다는 것을 안다면 일의 대부분은 이미 되어 있습니다.

전송 독립성 때문에 프로시저가 몇 번 실행되는지도 이 프로토콜이 약속하지 않습니다. RPC 프로토콜은 원격 프로시저나 그 실행 요건에 특정한 의미론을 붙이지 않습니다. 의미론은 아래 전송 프로토콜에서 추론할 수 있지만, 명세는 그것을 명시적으로 적어 두어야 한다고 말합니다. 남는 것은 아래와 같은 추론뿐입니다.

아래 전송 응답을 받으면 응답을 못 받으면
UDP 같은 신뢰성 없는 전송 프로시저가 적어도 한 번 실행됐다고 추론할 수 있습니다 실행 횟수에 관해 아무것도 추론할 수 없습니다
TCP 같은 신뢰성 있는 전송 프로시저가 정확히 한 번 실행됐다고 추론할 수 있습니다 원격 프로시저가 실행되지 않았다고 가정할 수 없습니다

가정이 깨질 때 응용이 떠안는 것이 재전송과 중복입니다. 응용이 타임아웃 뒤에 호출 메시지를 재전송하고도 응답을 못 받으면 실행 횟수를 알 길이 없습니다. 명세는 여기에 부분적인 대응 하나를 적어 둡니다. 서버가 앞서 처리한 요청을 기억해 두고 다시 처리하지 않으면 어느 정도의 execute-at-most-once 의미론을 확보할 수 있습니다. 서버는 모든 RPC 메시지에 붙어 오는 xid 를 이용해 이렇게 할 수 있습니다. 클라이언트 응용이 호출을 재전송할 때 이전 xid 를 다시 쓰기로 고를 수도 있습니다. 서버는 호출을 실행한 뒤 그 식별자를 기억해 두고 같은 식별자의 호출을 실행하지 않기로 고를 수도 있습니다. 다만 서버는 이 식별자를 같은지 비교하는 것 말고 다른 방식으로 들여다볼 수 없습니다.

관련 항목

메시지를 실어 나르는 아래 전송 프로토콜

TCP(Transmission Control Protocol, 전송 제어 프로토콜) · UDP(User Datagram Protocol, 사용자 데이터그램 프로토콜) · VMTP

맞세워지는 다른 호출 방식

로컬 프로시저 호출 · gRPC · JSON-RPC 2.0

메시지를 실제로 바이트로 적어 내는 부품

XDR · 스텁 · 마샬링 · IDL

정의하거나 기대는 RFC 문서

RFC 5531 · RFC 4506 · RFC 1813

표준화하거나 번호를 관리하는 주체

IANA · IETF · Sun Microsystems

명세가 밖으로 미루는 몫

바인딩 · 동시성 · 타임아웃 · 재전송

호출자의 신원 확인에 관여하는 요소

인증 · 검증자 · AUTH_ERROR · 세션

트랜잭션마다 바뀌는 역할

클라이언트 · 서버 · 트랜잭션

이것에서 자주 나는 오류·장애

RPC_MISMATCH · PROG_UNAVAIL · PROG_MISMATCH · PROC_UNAVAIL · GARBAGE_ARGS · SYSTEM_ERR · AUTH_BADCRED · AUTH_REJECTEDCRED · AUTH_BADVERF · AUTH_REJECTEDVERF · AUTH_TOOWEAK · AUTH_INVALIDRESP · AUTH_FAILED · RPCSEC_GSS_CREDPROBLEM · RPCSEC_GSS_CTXPROBLEM

다른 이름: Remote Procedure Call · 원격 프로시저 호출 · ONC RPC