Β§48.1–48.2Communication Basics and Unreliable Communication Layers

Part III OSTEP pp. 605–608 Β· ~6 min read

  • distributed system
  • packet loss
  • unreliable communication layer
  • udp
  • datagram

Distributed systems run across many machines β€” thousands, for a service like Google or Facebook β€” that cooperate to look, to you, like one always-available whole. The new problem they all share is failure: machines, disks, networks, and software all fail from time to time, and we don’t know how to build perfect parts.

The Crux: How To Build Systems That Work When Components Fail

How can we build a working system out of parts that don’t work correctly all the time? (You met a gentler version of this in RAID β€” here it’s harder.)

Failure is also the opportunity: a machine failing doesn’t have to mean the system fails. Pool enough machines and you can build something that rarely seems to fail even though its parts fail constantly. We start with the one genuinely new ingredient β€” communication β€” and its central, humbling truth.

48.1 Communication Basics

Tip: Communication Is Inherently Unreliable

Treat communication as a fundamentally unreliable activity. Bit corruption, down links and machines, and lack of buffer space all lead to the same result: packets sometimes don’t arrive. Reliable services must be built to cope.

Packet loss has several causes, and the most fundamental one is subtle:

bits flip in transit0110⚑a bit gets corruptedlinks & machines failβœ—a cable is cut, a node diesbuffers overflowrouter buffer (full)βœ—an arriving packet is dropped
Why packets go missing. Bits flip in transit; links get severed and machines die; and β€” even if everything works β€” a router or host with a full buffer has no choice but to drop an arriving packet.

That last cause is the deep one. Even if every link and machine worked perfectly, a packet arriving at a router must be stored in memory to be processed. If packets arrive faster than they leave, that memory fills, and the router simply drops what it can’t hold. The same happens at a busy end host. So loss isn’t an anomaly β€” it’s built into networking. The question is how to cope.

48.2 Unreliable Communication Layers

The simplest answer is: don’t cope β€” expose a raw, best-effort layer and let applications that already know how to tolerate loss handle it themselves. (That’s a preview of the end-to-end argument, at the end of the chapter.) The canonical unreliable communication layer is UDP/IP : a process uses the sockets API to make an endpoint, and machines exchange datagrams β€” fixed-size, best-effort messages.

A tiny client and server are all you need to start (Figure 48.1):

// client.c β€” send one message, await a reply
int main(int argc, char *argv[]) {
  int sd = UDP_Open(20000);
  struct sockaddr_in addrSnd, addrRcv;
  int rc = UDP_FillSockAddr(&addrSnd, "cs.wisc.edu", 10000);
  char message[BUFFER_SIZE];
  sprintf(message, "hello world");
  rc = UDP_Write(sd, &addrSnd, message, BUFFER_SIZE);
  if (rc > 0)
    rc = UDP_Read(sd, &addrRcv, message, BUFFER_SIZE);
  return 0;
}

// server.c β€” receive, then reply
int main(int argc, char *argv[]) {
  int sd = UDP_Open(10000);
  assert(sd > -1);
  while (1) {
    struct sockaddr_in addr;
    char message[BUFFER_SIZE];
    int rc = UDP_Read(sd, &addr, message, BUFFER_SIZE);
    if (rc > 0) {
      char reply[BUFFER_SIZE];
      sprintf(reply, "goodbye world");
      rc = UDP_Write(sd, &addr, reply, BUFFER_SIZE);
    }
  }
  return 0;
}

Those UDP_* calls are a thin wrapper over the sockets API (Figure 48.2) β€” the essentials:

int UDP_Open(int port) {                       // make a socket, bind to a port
  int sd = socket(AF_INET, SOCK_DGRAM, 0);
  struct sockaddr_in myaddr;
  bzero(&myaddr, sizeof(myaddr));
  myaddr.sin_family      = AF_INET;
  myaddr.sin_port        = htons(port);
  myaddr.sin_addr.s_addr = INADDR_ANY;
  bind(sd, (struct sockaddr *) &myaddr, sizeof(myaddr));
  return sd;
}
int UDP_Write(int sd, struct sockaddr_in *addr, char *buffer, int n) {
  int addr_len = sizeof(struct sockaddr_in);   // sendto() β€” fire off a datagram
  return sendto(sd, buffer, n, 0, (struct sockaddr *) addr, addr_len);
}
int UDP_Read(int sd, struct sockaddr_in *addr, char *buffer, int n) {
  int len = sizeof(struct sockaddr_in);        // recvfrom() β€” receive a datagram
  return recvfrom(sd, buffer, n, 0, (struct sockaddr *) addr, (socklen_t *) &len);
}

The exchange looks like this:

clientport 20000serverport 10000datagram: "hello world" →← "goodbye world"…but either datagram may simply vanish β€” UDP won't tell you
Figure 48.1’s client/server: a β€œhello world” datagram out, a β€œgoodbye world” reply back. Either one can vanish, and UDP will never tell the sender.

UDP isn’t entirely defenseless: it carries a checksum to catch some corrupted packets.

Tip: Use Checksums For Integrity

Before sending, compute a checksum over the message’s bytes and send both. The receiver recomputes it over what arrived and compares; a match gives assurance the data wasn’t corrupted in transit. (The same store-and-recompare idea from data integrity β€” stronger checksums cost more to compute.)

But a checksum only catches corruption β€” it does nothing about loss, and when a datagram is dropped the sender is never told. Since most applications just want their data to arrive, we need more: reliable communication built on top of this unreliable network β€” the subject of the next section.

Check yourself: unreliable communication

1.Even if every link worked perfectly and every machine stayed up, why is packet loss still fundamental?

2.What does UDP, as an unreliable communication layer, give you and NOT give you?

3.Why might a system deliberately expose a raw unreliable layer instead of always providing reliability?

4.What is a datagram in the UDP model?

5.How are checksums used to protect a message crossing the network?

5 questions