Проблематика

Чтобы оценить производительность ввода/вывода обычно принято обращаться к метрикам дисковой подсистемы, благо они богато представлены “стандартными” инструментами linux - sar, iostat, atop, etc.

Проблема данного подхода, в упрощении общей картины, он игнорирует такой важный компонент в работе I/O подсистемы как файловая система.

Например:

  • приложение может использовать асинхронную модель записи и flush на диск происходит порциями и когда-то потом;
  • приложение хочет прочитать 1 байт данных, но считать с диска возможно только блоком в 4096 байт;
  • используется read-ahead при последовательном чтении;
  • чтение происходит из кеша файловой системы - запросов на чтение много, а диск почти не загружен;

и так далее.

Все это может вводить в заблуждение и в моменте неправильно интерпретироваться.

(инструментацию ввода/вывода приложения оставлю за скобками - дорого, сложно масштабировать и редко встречается)

Выход есть

В проекте iovisor/bcc есть множество инструментов для исследования работы linux.

В частности утилиты xfsdist, ext4dist, zfsdist, btrfsdist, nfsdist помогают строить гистограммы задержек операций уровня файловой системы - read() , write(), sync(), open():

# https://github.com/iovisor/bcc/blob/master/tools/xfsdist_example.txt
$ ./xfsdist 
Tracing XFS operation latency... Hit Ctrl-C to end.
^C
                                  |
operation = b'read'
     usecs               : count     distribution
         0 -> 1          : 421818   |******************************|
         2 -> 3          : 270492   |****************               
         4 -> 7          : 4634     |                                        
         8 -> 15         : 2723     |                                        
        16 -> 31         : 1089     |                                        
        32 -> 63         : 1558     |                                        
        64 -> 127        : 1024     |                                        
       128 -> 255        : 921      |                                        
       256 -> 511        : 146      |                                        
       512 -> 1023       : 1        |                                        
      1024 -> 2047       : 2        |                                        
      2048 -> 4095       : 3        |                                        
      4096 -> 8191       : 34       |                                                                          

operation = b'write'
     usecs               : count     distribution
         0 -> 1          : 324      |                                        
         2 -> 3          : 30594    |******************************|
         4 -> 7          : 7869     |*****                              
         8 -> 15         : 3267     |***                                    
        16 -> 31         : 1373     |*                                       
        32 -> 63         : 401      |                                        
        64 -> 127        : 56       |                                        
       128 -> 255        : 24       |                                        
       256 -> 511        : 3        |                                        
       512 -> 1023       : 1        |                                        

... 

Для получения задержек на уровне дисков можно задействовать утилиту biolatency:

# <https://github.com/iovisor/bcc/blob/master/tools/biolatency_example.txt>
$ ./biolatency

flags = Read
     usecs               : count     distribution
         0 -> 1          : 0        |                                  
         2 -> 3          : 0        |                                        
         4 -> 7          : 0        |                                        
         8 -> 15         : 0        |                                        
        16 -> 31         : 265      |***********                          
        32 -> 63         : 707      |*****************************|
        64 -> 127        : 95       |****                                   
       128 -> 255        : 45       |*                                      

flags = Write
     usecs               : count     distribution
         0 -> 1          : 0        |                                        
         2 -> 3          : 0        |                                        
         4 -> 7          : 0        |                                        
         8 -> 15         : 0        |                                        
        16 -> 31         : 0        |                                        
        32 -> 63         : 37       |*                                       
        64 -> 127        : 123      |******                                  
       128 -> 255        : 741      |*****************************|
       256 -> 511        : 636      |*************************      
       512 -> 1023       : 93       |*****                                   
      1024 -> 2047       : 11       |                                        
... 

Сопоставив оба сниппета можно сделать выводы:

  • чтение (всегда синхронная операция) в основном удовлетворяются из кеша файловой системы - до диска доходит лишь малая доля запросов и с точки зрения приложения чтение завершается за миллисекунды;
  • операции записи в массе асинхронные и скопом скидываются на диск позже (write-back) - опять же различные объемы и длительность операций.

Подобные метрики дают возможность:

  1. пролить свет на работу файловых систем;
  2. отделить задержки уровня ФС от дисков, что на круг позволит делать более точные выводы о их работы и упрощать поиск корневых причин в случае проблем.

Немного картинок в grafana

Для лучшей визуализации и анализа исторических данных я адаптировал xfsdist (PR) и ext4dist (PR) утилиты под использование в ebpf_exporter, теперь они доступны в grafana:

node_exporter

А так это выглядит на процентилях:

node_exporter

Примеры PromQL-запросов:

  • гистограмма:
    sum(rate(ebpf_exporter_xfs_latency_seconds_bucket{operation="read"}[$__rate_interval])) by (le)
    
  • процентили:
    histogram_quantile(0.90, sum(rate(ebpf_exporter_bio_latency_seconds_bucket{operation="read"}[$__rate_interval])) by (le, instance))
    

Подобные метрики должны быть по душе владельцам нагруженных IO сервисов, которые хотят быть чуть глубже осведомлены о поведении своих систем.

Удачи!