@@ -100,13 +100,34 @@ def sum_stat(stat)
100100 class SocketData
101101 UNACKED_REGEXP = /\ unacked=(?<unacked>\d +)\ / . freeze
102102
103- def initialize ( ios )
104- @sockets = ios . select { |io | io . respond_to? ( :getsockopt ) }
103+ def initialize ( ios , parser )
104+ @sockets = ios . select { |io | io . respond_to? ( :getsockopt ) && io . is_a? ( TCPSocket ) }
105+ @parser =
106+ case parser
107+ when :inspect then method ( :parse_with_inspect )
108+ when :unpack then method ( :parse_with_unpack )
109+ when Proc then parser
110+ end
105111 end
106112
107113 # Number of unacknowledged connections in the sockets, which
108114 # we know as socket backlog.
109115 #
116+ def unacked
117+ @sockets . sum do |socket |
118+ @parser . call ( socket . getsockopt ( Socket ::SOL_TCP ,
119+ Socket ::TCP_INFO ) )
120+ end
121+ end
122+
123+ def metrics
124+ {
125+ "sockets.backlog" => unacked
126+ }
127+ end
128+
129+ private
130+
110131 # The Socket::Option returned by `getsockopt` doesn't provide
111132 # any kind of accessors for data inside. It decodes it on demand
112133 # for `inspect` as strings in C implementation. It looks like
@@ -143,21 +164,104 @@ def initialize(ios)
143164 # total_retrans=0
144165 # (128 bytes too long)>
145166 #
146- # That's why we have to pull the `unacked` field by parsing
147- # `inspect` output, instead of using something like `opt.unacked`
148- def unacked
149- @sockets . sum do |socket |
150- tcp_info = socket . getsockopt ( Socket ::SOL_TCP , Socket ::TCP_INFO ) . inspect
151- tcp_match = tcp_info . match ( UNACKED_REGEXP )
167+ # That's why pulling the `unacked` field by parsing
168+ # `inspect` output is one of the ways to retrieve it.
169+ #
170+ def parse_with_inspect ( tcp_info )
171+ tcp_match = tcp_info . inspect . match ( UNACKED_REGEXP )
152172
153- tcp_match [ :unacked ] . to_i
154- end
173+ return 0 if tcp_match . nil?
174+
175+ tcp_match [ :unacked ] . to_i
155176 end
156177
157- def metrics
158- {
159- "sockets.backlog" => unacked
160- }
178+ # The above inspect data might not be available everywhere (looking at you
179+ # AWS Fargate Host running on kernel 4.14!), but we might still recover it
180+ # by manually unpacking the binary data based on linux headers. For example
181+ # below is tcp info struct from `linux/tcp.h` header file, from problematic
182+ # host rocking kernel 4.14.
183+ #
184+ # struct tcp_info {
185+ # __u8 tcpi_state;
186+ # __u8 tcpi_ca_state;
187+ # __u8 tcpi_retransmits;
188+ # __u8 tcpi_probes;
189+ # __u8 tcpi_backoff;
190+ # __u8 tcpi_options;
191+ # __u8 tcpi_snd_wscale : 4, tcpi_rcv_wscale : 4;
192+ # __u8 tcpi_delivery_rate_app_limited:1;
193+ #
194+ # __u32 tcpi_rto;
195+ # __u32 tcpi_ato;
196+ # __u32 tcpi_snd_mss;
197+ # __u32 tcpi_rcv_mss;
198+ #
199+ # __u32 tcpi_unacked;
200+ # __u32 tcpi_sacked;
201+ # __u32 tcpi_lost;
202+ # __u32 tcpi_retrans;
203+ # __u32 tcpi_fackets;
204+ #
205+ # /* Times. */
206+ # __u32 tcpi_last_data_sent;
207+ # __u32 tcpi_last_ack_sent; /* Not remembered, sorry. */
208+ # __u32 tcpi_last_data_recv;
209+ # __u32 tcpi_last_ack_recv;
210+ #
211+ # /* Metrics. */
212+ # __u32 tcpi_pmtu;
213+ # __u32 tcpi_rcv_ssthresh;
214+ # __u32 tcpi_rtt;
215+ # __u32 tcpi_rttvar;
216+ # __u32 tcpi_snd_ssthresh;
217+ # __u32 tcpi_snd_cwnd;
218+ # __u32 tcpi_advmss;
219+ # __u32 tcpi_reordering;
220+ #
221+ # __u32 tcpi_rcv_rtt;
222+ # __u32 tcpi_rcv_space;
223+ #
224+ # __u32 tcpi_total_retrans;
225+ #
226+ # __u64 tcpi_pacing_rate;
227+ # __u64 tcpi_max_pacing_rate;
228+ # __u64 tcpi_bytes_acked; /* RFC4898 tcpEStatsAppHCThruOctetsAcked */
229+ # __u64 tcpi_bytes_received; /* RFC4898 tcpEStatsAppHCThruOctetsReceived */
230+ # __u32 tcpi_segs_out; /* RFC4898 tcpEStatsPerfSegsOut */
231+ # __u32 tcpi_segs_in; /* RFC4898 tcpEStatsPerfSegsIn */
232+ #
233+ # __u32 tcpi_notsent_bytes;
234+ # __u32 tcpi_min_rtt;
235+ # __u32 tcpi_data_segs_in; /* RFC4898 tcpEStatsDataSegsIn */
236+ # __u32 tcpi_data_segs_out; /* RFC4898 tcpEStatsDataSegsOut */
237+ #
238+ # __u64 tcpi_delivery_rate;
239+ #
240+ # __u64 tcpi_busy_time; /* Time (usec) busy sending data */
241+ # __u64 tcpi_rwnd_limited; /* Time (usec) limited by receive window */
242+ # __u64 tcpi_sndbuf_limited; /* Time (usec) limited by send buffer */
243+ # };
244+ #
245+ # Now nowing types and order of fields we can easily parse binary data
246+ # by using
247+ # - `C` flag for `__u8` type - 8-bit unsigned (unsigned char)
248+ # - `L` flag for `__u32` type - 32-bit unsigned, native endian (uint32_t)
249+ # - `Q` flag for `__u64` type - 64-bit unsigned, native endian (uint64_t)
250+ #
251+ # Complete `unpack` would look like `C8 L24 Q4 L6 Q4`, but we are only
252+ # interested in `unacked` field at the moment, that's why we only parse
253+ # till this field by unpacking with `C8 L5`.
254+ #
255+ # If you find that it's not giving correct results, then please fall back
256+ # to inspect, or update this code to accept unpack sequence. But in the
257+ # end unpack is preferable, as it's 12x faster than inspect.
258+ #
259+ # Tested against:
260+ # - Amazon Linux 2 with kernel 4.14 & 5.10
261+ # - Ubuntu 20.04 with kernel 5.13
262+ #
263+ def parse_with_unpack ( tcp_info )
264+ tcp_info . unpack ( "C8L5" ) . last
161265 end
162266 end
163267 end
0 commit comments