P4: Traffic Monitoring

Many network applications depend on being able to monitor the state of the network. There are a variety of questions we mighht wish to ask including:

  • What is the topology of the network?
  • How much traffic is flowing across a particular path?
  • What are the largest sources of traffic?

Given this information, there are several actions that a network application might take such as:

  • Computing the traffic matrix – i.e., the amount of traffic flowing between each source-destination pair.
  • Blocking unauthorized traffic flows – i.e., according to the access control policy.
  • Implementing traffic engineering – i.e., shifting load away from congested links.

In this lecture, we will see how a control-plane can collect information from the data plane using counters.

P4 Counters

The V1 Model provides a counter extern object that can be used to implement a wide range of monitoring tasks. The declaration for counter objects,

extern counter {
    counter(bit<32> size, CounterType type);
    void count(in bit<32> index);
}

includes a constructor counter and a method count for actually counting packets. Counters are allocated similarly to arrays in general purpose languages. Accordingly, the constructor is parameterized on a size parameter that specifies how many counters to create, as well as a CounterType parameter that specifies the granularity of the counters: packets, bytes, or both.

The granularity of each counter, packets, bytes or both, can be specified using the following enum:

enum CounterType {
    packets,
    bytes,
    packets_and_bytes
}

Example

As an example to illustrate the usage of counters, suppose that we have an existing control that forwards IPv4 packets. We can add fine-grained monitoring functionality as follows:

control MyIngress(..., inout headers hdr, ...) {
  counter(64, CounterType.packets) c;
  
  action tally() {
    c.count((bit<32>) standard_metadata.ingress_port);
  } 

  table monitor {
    key = { 
      hdr.ipv4.srcAddr: lpm;
    } 
    actions = { tally; NoAction; }
  } 

  apply { 
    ...
    if(hdr.ipv4.isValid()) {
      ...
      monitor.apply();
    } 
  } 
}

There are several things going on in this example code. First, the declaration creates a packet counter c with 64 entries, one for each physical port on the switch. The tally action increments this counter, using the ingress port as the index into the array. Finally the monitor table matches on the source IPv4 address and either executes tally action or NoAction, which is a no-op. Hence, the control plane can selectively monitor the amount of traffic being sent from specific IPv4 prefixes, aggregated by ingress port.

Run-Time API

Counters have no effect on the execution of the data-plane program. That is, it is impossible to read the value of a counter in a P4 program.

However, the value of a counter can be queried by the control plane. For example, the simple_switch_CLI utility provides an operation counter_read that takes the name of a counter and an index and returns the packet/byte counters associated with that counter. If we populate the monitor table with a forwarding rule that matches on packets coming from 10.0.1.1,

{
  "table": "MyIngress.monitor",
  "match": {
    "hdr.ipv4.srcAddr": ["10.0.1.1", 32]
  },
  "action_name": "MyIngress.tally",
  "action_params": { }
}

and send a packet from 10.0.1.1 to any other host, then querying the counter will return a value such as the following:

RuntimeCmd: counter_read MyIngress.c 1
MyIngress.c[1]=  BmCounterValue(packets=1, bytes=59)

Note that the Bmv2 implementation of counters keeps track of both packet and byte counters by default, even though the counter itself was created with CounterType.packets.

Direct Counters

In many applications, it is useful to associate counters directly with the entries in a match-action table. In paricular, rather than having to specify the length of the array and the index on each count operation, we can use the size of the table and the index of the table entry.

The V1Model architecture includes a second counter-like primitive, direct_counter that provides this functionality:

extern direct_counter {
    direct_counter(CounterType type);
    void count();
}

Note that compared to the counter object, the size and index arguments have been elided.

Example

To use a direct counter a program, we can simply add the counters attribute to a table. The example from above can be rewritten as follows:

control MyIngress(..., inout headers hdr, ...) {
  direct_counter(CounterType.packets) c;
  
  action tally() {
    c.count();
  } 

  table monitor {
    key = { 
      hdr.ipv4.srcAddr: lpm;
    } 
    actions = { tally; NoAction; }
    counters = c;
  } 

  apply { 
    ...
    if(hdr.ipv4.isValid()) {
      ...
      monitor.apply();
    } 
  } 
}

The run-time API for direct counters is identical to the API for standard counters. Note, however that the Bmv2 API has a small bug – it does not provide a way to access the counter associated with the table’s default action.

Discussion

Although counters are quite limited as a primitive, when combined with a dynamic control plane, they can be used to implement more sophisticated monitoring tasks while staying within the resource constraints of network devices such as hardware switches.

In particular, using a table like monitor above, the control-plane can “slice and dice” into aggregates of varying-size, using the semantics of LPM tables on IP prefixes. For example, the control-plane could install an initial set of forwarding rules that monitor relatively coarse-grained aggregates, but then “drill down” to finer-grained aggregates as the mix of traffic evolves. In the limit, it can monitor the traffic from individual hosts.

Reading

For detailed case study showing how counters can be used to implement more sophisticated monitoring tasks, see the (HotICE ‘13 paper)Jose13 “Online measurement of large traffic aggregates on commodity switches.”