mountstats.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  1. package procfs
  2. // While implementing parsing of /proc/[pid]/mountstats, this blog was used
  3. // heavily as a reference:
  4. // https://utcc.utoronto.ca/~cks/space/blog/linux/NFSMountstatsIndex
  5. //
  6. // Special thanks to Chris Siebenmann for all of his posts explaining the
  7. // various statistics available for NFS.
  8. import (
  9. "bufio"
  10. "fmt"
  11. "io"
  12. "strconv"
  13. "strings"
  14. "time"
  15. )
  16. // Constants shared between multiple functions.
  17. const (
  18. deviceEntryLen = 8
  19. fieldBytesLen = 8
  20. fieldEventsLen = 27
  21. statVersion10 = "1.0"
  22. statVersion11 = "1.1"
  23. fieldTransport10Len = 10
  24. fieldTransport11Len = 13
  25. )
  26. // A Mount is a device mount parsed from /proc/[pid]/mountstats.
  27. type Mount struct {
  28. // Name of the device.
  29. Device string
  30. // The mount point of the device.
  31. Mount string
  32. // The filesystem type used by the device.
  33. Type string
  34. // If available additional statistics related to this Mount.
  35. // Use a type assertion to determine if additional statistics are available.
  36. Stats MountStats
  37. }
  38. // A MountStats is a type which contains detailed statistics for a specific
  39. // type of Mount.
  40. type MountStats interface {
  41. mountStats()
  42. }
  43. // A MountStatsNFS is a MountStats implementation for NFSv3 and v4 mounts.
  44. type MountStatsNFS struct {
  45. // The version of statistics provided.
  46. StatVersion string
  47. // The age of the NFS mount.
  48. Age time.Duration
  49. // Statistics related to byte counters for various operations.
  50. Bytes NFSBytesStats
  51. // Statistics related to various NFS event occurrences.
  52. Events NFSEventsStats
  53. // Statistics broken down by filesystem operation.
  54. Operations []NFSOperationStats
  55. // Statistics about the NFS RPC transport.
  56. Transport NFSTransportStats
  57. }
  58. // mountStats implements MountStats.
  59. func (m MountStatsNFS) mountStats() {}
  60. // A NFSBytesStats contains statistics about the number of bytes read and written
  61. // by an NFS client to and from an NFS server.
  62. type NFSBytesStats struct {
  63. // Number of bytes read using the read() syscall.
  64. Read int
  65. // Number of bytes written using the write() syscall.
  66. Write int
  67. // Number of bytes read using the read() syscall in O_DIRECT mode.
  68. DirectRead int
  69. // Number of bytes written using the write() syscall in O_DIRECT mode.
  70. DirectWrite int
  71. // Number of bytes read from the NFS server, in total.
  72. ReadTotal int
  73. // Number of bytes written to the NFS server, in total.
  74. WriteTotal int
  75. // Number of pages read directly via mmap()'d files.
  76. ReadPages int
  77. // Number of pages written directly via mmap()'d files.
  78. WritePages int
  79. }
  80. // A NFSEventsStats contains statistics about NFS event occurrences.
  81. type NFSEventsStats struct {
  82. // Number of times cached inode attributes are re-validated from the server.
  83. InodeRevalidate int
  84. // Number of times cached dentry nodes are re-validated from the server.
  85. DnodeRevalidate int
  86. // Number of times an inode cache is cleared.
  87. DataInvalidate int
  88. // Number of times cached inode attributes are invalidated.
  89. AttributeInvalidate int
  90. // Number of times files or directories have been open()'d.
  91. VFSOpen int
  92. // Number of times a directory lookup has occurred.
  93. VFSLookup int
  94. // Number of times permissions have been checked.
  95. VFSAccess int
  96. // Number of updates (and potential writes) to pages.
  97. VFSUpdatePage int
  98. // Number of pages read directly via mmap()'d files.
  99. VFSReadPage int
  100. // Number of times a group of pages have been read.
  101. VFSReadPages int
  102. // Number of pages written directly via mmap()'d files.
  103. VFSWritePage int
  104. // Number of times a group of pages have been written.
  105. VFSWritePages int
  106. // Number of times directory entries have been read with getdents().
  107. VFSGetdents int
  108. // Number of times attributes have been set on inodes.
  109. VFSSetattr int
  110. // Number of pending writes that have been forcefully flushed to the server.
  111. VFSFlush int
  112. // Number of times fsync() has been called on directories and files.
  113. VFSFsync int
  114. // Number of times locking has been attemped on a file.
  115. VFSLock int
  116. // Number of times files have been closed and released.
  117. VFSFileRelease int
  118. // Unknown. Possibly unused.
  119. CongestionWait int
  120. // Number of times files have been truncated.
  121. Truncation int
  122. // Number of times a file has been grown due to writes beyond its existing end.
  123. WriteExtension int
  124. // Number of times a file was removed while still open by another process.
  125. SillyRename int
  126. // Number of times the NFS server gave less data than expected while reading.
  127. ShortRead int
  128. // Number of times the NFS server wrote less data than expected while writing.
  129. ShortWrite int
  130. // Number of times the NFS server indicated EJUKEBOX; retrieving data from
  131. // offline storage.
  132. JukeboxDelay int
  133. // Number of NFS v4.1+ pNFS reads.
  134. PNFSRead int
  135. // Number of NFS v4.1+ pNFS writes.
  136. PNFSWrite int
  137. }
  138. // A NFSOperationStats contains statistics for a single operation.
  139. type NFSOperationStats struct {
  140. // The name of the operation.
  141. Operation string
  142. // Number of requests performed for this operation.
  143. Requests int
  144. // Number of times an actual RPC request has been transmitted for this operation.
  145. Transmissions int
  146. // Number of times a request has had a major timeout.
  147. MajorTimeouts int
  148. // Number of bytes sent for this operation, including RPC headers and payload.
  149. BytesSent int
  150. // Number of bytes received for this operation, including RPC headers and payload.
  151. BytesReceived int
  152. // Duration all requests spent queued for transmission before they were sent.
  153. CumulativeQueueTime time.Duration
  154. // Duration it took to get a reply back after the request was transmitted.
  155. CumulativeTotalResponseTime time.Duration
  156. // Duration from when a request was enqueued to when it was completely handled.
  157. CumulativeTotalRequestTime time.Duration
  158. }
  159. // A NFSTransportStats contains statistics for the NFS mount RPC requests and
  160. // responses.
  161. type NFSTransportStats struct {
  162. // The local port used for the NFS mount.
  163. Port int
  164. // Number of times the client has had to establish a connection from scratch
  165. // to the NFS server.
  166. Bind int
  167. // Number of times the client has made a TCP connection to the NFS server.
  168. Connect int
  169. // Duration (in jiffies, a kernel internal unit of time) the NFS mount has
  170. // spent waiting for connections to the server to be established.
  171. ConnectIdleTime int
  172. // Duration since the NFS mount last saw any RPC traffic.
  173. IdleTime time.Duration
  174. // Number of RPC requests for this mount sent to the NFS server.
  175. Sends int
  176. // Number of RPC responses for this mount received from the NFS server.
  177. Receives int
  178. // Number of times the NFS server sent a response with a transaction ID
  179. // unknown to this client.
  180. BadTransactionIDs int
  181. // A running counter, incremented on each request as the current difference
  182. // ebetween sends and receives.
  183. CumulativeActiveRequests int
  184. // A running counter, incremented on each request by the current backlog
  185. // queue size.
  186. CumulativeBacklog int
  187. // Stats below only available with stat version 1.1.
  188. // Maximum number of simultaneously active RPC requests ever used.
  189. MaximumRPCSlotsUsed int
  190. // A running counter, incremented on each request as the current size of the
  191. // sending queue.
  192. CumulativeSendingQueue int
  193. // A running counter, incremented on each request as the current size of the
  194. // pending queue.
  195. CumulativePendingQueue int
  196. }
  197. // parseMountStats parses a /proc/[pid]/mountstats file and returns a slice
  198. // of Mount structures containing detailed information about each mount.
  199. // If available, statistics for each mount are parsed as well.
  200. func parseMountStats(r io.Reader) ([]*Mount, error) {
  201. const (
  202. device = "device"
  203. statVersionPrefix = "statvers="
  204. nfs3Type = "nfs"
  205. nfs4Type = "nfs4"
  206. )
  207. var mounts []*Mount
  208. s := bufio.NewScanner(r)
  209. for s.Scan() {
  210. // Only look for device entries in this function
  211. ss := strings.Fields(string(s.Bytes()))
  212. if len(ss) == 0 || ss[0] != device {
  213. continue
  214. }
  215. m, err := parseMount(ss)
  216. if err != nil {
  217. return nil, err
  218. }
  219. // Does this mount also possess statistics information?
  220. if len(ss) > deviceEntryLen {
  221. // Only NFSv3 and v4 are supported for parsing statistics
  222. if m.Type != nfs3Type && m.Type != nfs4Type {
  223. return nil, fmt.Errorf("cannot parse MountStats for fstype %q", m.Type)
  224. }
  225. statVersion := strings.TrimPrefix(ss[8], statVersionPrefix)
  226. stats, err := parseMountStatsNFS(s, statVersion)
  227. if err != nil {
  228. return nil, err
  229. }
  230. m.Stats = stats
  231. }
  232. mounts = append(mounts, m)
  233. }
  234. return mounts, s.Err()
  235. }
  236. // parseMount parses an entry in /proc/[pid]/mountstats in the format:
  237. // device [device] mounted on [mount] with fstype [type]
  238. func parseMount(ss []string) (*Mount, error) {
  239. if len(ss) < deviceEntryLen {
  240. return nil, fmt.Errorf("invalid device entry: %v", ss)
  241. }
  242. // Check for specific words appearing at specific indices to ensure
  243. // the format is consistent with what we expect
  244. format := []struct {
  245. i int
  246. s string
  247. }{
  248. {i: 0, s: "device"},
  249. {i: 2, s: "mounted"},
  250. {i: 3, s: "on"},
  251. {i: 5, s: "with"},
  252. {i: 6, s: "fstype"},
  253. }
  254. for _, f := range format {
  255. if ss[f.i] != f.s {
  256. return nil, fmt.Errorf("invalid device entry: %v", ss)
  257. }
  258. }
  259. return &Mount{
  260. Device: ss[1],
  261. Mount: ss[4],
  262. Type: ss[7],
  263. }, nil
  264. }
  265. // parseMountStatsNFS parses a MountStatsNFS by scanning additional information
  266. // related to NFS statistics.
  267. func parseMountStatsNFS(s *bufio.Scanner, statVersion string) (*MountStatsNFS, error) {
  268. // Field indicators for parsing specific types of data
  269. const (
  270. fieldAge = "age:"
  271. fieldBytes = "bytes:"
  272. fieldEvents = "events:"
  273. fieldPerOpStats = "per-op"
  274. fieldTransport = "xprt:"
  275. )
  276. stats := &MountStatsNFS{
  277. StatVersion: statVersion,
  278. }
  279. for s.Scan() {
  280. ss := strings.Fields(string(s.Bytes()))
  281. if len(ss) == 0 {
  282. break
  283. }
  284. if len(ss) < 2 {
  285. return nil, fmt.Errorf("not enough information for NFS stats: %v", ss)
  286. }
  287. switch ss[0] {
  288. case fieldAge:
  289. // Age integer is in seconds
  290. d, err := time.ParseDuration(ss[1] + "s")
  291. if err != nil {
  292. return nil, err
  293. }
  294. stats.Age = d
  295. case fieldBytes:
  296. bstats, err := parseNFSBytesStats(ss[1:])
  297. if err != nil {
  298. return nil, err
  299. }
  300. stats.Bytes = *bstats
  301. case fieldEvents:
  302. estats, err := parseNFSEventsStats(ss[1:])
  303. if err != nil {
  304. return nil, err
  305. }
  306. stats.Events = *estats
  307. case fieldTransport:
  308. if len(ss) < 3 {
  309. return nil, fmt.Errorf("not enough information for NFS transport stats: %v", ss)
  310. }
  311. tstats, err := parseNFSTransportStats(ss[2:], statVersion)
  312. if err != nil {
  313. return nil, err
  314. }
  315. stats.Transport = *tstats
  316. }
  317. // When encountering "per-operation statistics", we must break this
  318. // loop and parse them seperately to ensure we can terminate parsing
  319. // before reaching another device entry; hence why this 'if' statement
  320. // is not just another switch case
  321. if ss[0] == fieldPerOpStats {
  322. break
  323. }
  324. }
  325. if err := s.Err(); err != nil {
  326. return nil, err
  327. }
  328. // NFS per-operation stats appear last before the next device entry
  329. perOpStats, err := parseNFSOperationStats(s)
  330. if err != nil {
  331. return nil, err
  332. }
  333. stats.Operations = perOpStats
  334. return stats, nil
  335. }
  336. // parseNFSBytesStats parses a NFSBytesStats line using an input set of
  337. // integer fields.
  338. func parseNFSBytesStats(ss []string) (*NFSBytesStats, error) {
  339. if len(ss) != fieldBytesLen {
  340. return nil, fmt.Errorf("invalid NFS bytes stats: %v", ss)
  341. }
  342. ns := make([]int, 0, fieldBytesLen)
  343. for _, s := range ss {
  344. n, err := strconv.Atoi(s)
  345. if err != nil {
  346. return nil, err
  347. }
  348. ns = append(ns, n)
  349. }
  350. return &NFSBytesStats{
  351. Read: ns[0],
  352. Write: ns[1],
  353. DirectRead: ns[2],
  354. DirectWrite: ns[3],
  355. ReadTotal: ns[4],
  356. WriteTotal: ns[5],
  357. ReadPages: ns[6],
  358. WritePages: ns[7],
  359. }, nil
  360. }
  361. // parseNFSEventsStats parses a NFSEventsStats line using an input set of
  362. // integer fields.
  363. func parseNFSEventsStats(ss []string) (*NFSEventsStats, error) {
  364. if len(ss) != fieldEventsLen {
  365. return nil, fmt.Errorf("invalid NFS events stats: %v", ss)
  366. }
  367. ns := make([]int, 0, fieldEventsLen)
  368. for _, s := range ss {
  369. n, err := strconv.Atoi(s)
  370. if err != nil {
  371. return nil, err
  372. }
  373. ns = append(ns, n)
  374. }
  375. return &NFSEventsStats{
  376. InodeRevalidate: ns[0],
  377. DnodeRevalidate: ns[1],
  378. DataInvalidate: ns[2],
  379. AttributeInvalidate: ns[3],
  380. VFSOpen: ns[4],
  381. VFSLookup: ns[5],
  382. VFSAccess: ns[6],
  383. VFSUpdatePage: ns[7],
  384. VFSReadPage: ns[8],
  385. VFSReadPages: ns[9],
  386. VFSWritePage: ns[10],
  387. VFSWritePages: ns[11],
  388. VFSGetdents: ns[12],
  389. VFSSetattr: ns[13],
  390. VFSFlush: ns[14],
  391. VFSFsync: ns[15],
  392. VFSLock: ns[16],
  393. VFSFileRelease: ns[17],
  394. CongestionWait: ns[18],
  395. Truncation: ns[19],
  396. WriteExtension: ns[20],
  397. SillyRename: ns[21],
  398. ShortRead: ns[22],
  399. ShortWrite: ns[23],
  400. JukeboxDelay: ns[24],
  401. PNFSRead: ns[25],
  402. PNFSWrite: ns[26],
  403. }, nil
  404. }
  405. // parseNFSOperationStats parses a slice of NFSOperationStats by scanning
  406. // additional information about per-operation statistics until an empty
  407. // line is reached.
  408. func parseNFSOperationStats(s *bufio.Scanner) ([]NFSOperationStats, error) {
  409. const (
  410. // Number of expected fields in each per-operation statistics set
  411. numFields = 9
  412. )
  413. var ops []NFSOperationStats
  414. for s.Scan() {
  415. ss := strings.Fields(string(s.Bytes()))
  416. if len(ss) == 0 {
  417. // Must break when reading a blank line after per-operation stats to
  418. // enable top-level function to parse the next device entry
  419. break
  420. }
  421. if len(ss) != numFields {
  422. return nil, fmt.Errorf("invalid NFS per-operations stats: %v", ss)
  423. }
  424. // Skip string operation name for integers
  425. ns := make([]int, 0, numFields-1)
  426. for _, st := range ss[1:] {
  427. n, err := strconv.Atoi(st)
  428. if err != nil {
  429. return nil, err
  430. }
  431. ns = append(ns, n)
  432. }
  433. ops = append(ops, NFSOperationStats{
  434. Operation: strings.TrimSuffix(ss[0], ":"),
  435. Requests: ns[0],
  436. Transmissions: ns[1],
  437. MajorTimeouts: ns[2],
  438. BytesSent: ns[3],
  439. BytesReceived: ns[4],
  440. CumulativeQueueTime: time.Duration(ns[5]) * time.Millisecond,
  441. CumulativeTotalResponseTime: time.Duration(ns[6]) * time.Millisecond,
  442. CumulativeTotalRequestTime: time.Duration(ns[7]) * time.Millisecond,
  443. })
  444. }
  445. return ops, s.Err()
  446. }
  447. // parseNFSTransportStats parses a NFSTransportStats line using an input set of
  448. // integer fields matched to a specific stats version.
  449. func parseNFSTransportStats(ss []string, statVersion string) (*NFSTransportStats, error) {
  450. switch statVersion {
  451. case statVersion10:
  452. if len(ss) != fieldTransport10Len {
  453. return nil, fmt.Errorf("invalid NFS transport stats 1.0 statement: %v", ss)
  454. }
  455. case statVersion11:
  456. if len(ss) != fieldTransport11Len {
  457. return nil, fmt.Errorf("invalid NFS transport stats 1.1 statement: %v", ss)
  458. }
  459. default:
  460. return nil, fmt.Errorf("unrecognized NFS transport stats version: %q", statVersion)
  461. }
  462. // Allocate enough for v1.1 stats since zero value for v1.1 stats will be okay
  463. // in a v1.0 response
  464. ns := make([]int, 0, fieldTransport11Len)
  465. for _, s := range ss {
  466. n, err := strconv.Atoi(s)
  467. if err != nil {
  468. return nil, err
  469. }
  470. ns = append(ns, n)
  471. }
  472. return &NFSTransportStats{
  473. Port: ns[0],
  474. Bind: ns[1],
  475. Connect: ns[2],
  476. ConnectIdleTime: ns[3],
  477. IdleTime: time.Duration(ns[4]) * time.Second,
  478. Sends: ns[5],
  479. Receives: ns[6],
  480. BadTransactionIDs: ns[7],
  481. CumulativeActiveRequests: ns[8],
  482. CumulativeBacklog: ns[9],
  483. MaximumRPCSlotsUsed: ns[10],
  484. CumulativeSendingQueue: ns[11],
  485. CumulativePendingQueue: ns[12],
  486. }, nil
  487. }