บทความ Internet Protocol จบลงที่การพาแพ็กเก็ตไปถึงเครื่อง ปลายทาง และนั่นคือทั้งหมดที่ IP รับปาก
สองคำถามที่เหลืออยู่หลังจากนั้นสำคัญพอ ๆ กับคำถามที่มันตอบไปแล้ว
หน้านี้คือเรื่องของสองโพรโทคอลที่ตอบคำถามนั้นคนละแบบ และเหตุผลที่ทั้งคู่ยังอยู่
IP ส่งถึงเครื่อง แล้วจบ
แพ็กเก็ตมาถึงเครื่องแล้ว เครื่องนั้นรันโปรแกรมอยู่หลายสิบตัว คำถามแรกคือ ข้อมูลก้อนนี้เป็นของโปรแกรมไหน
และ IP ไม่เคยรับปากว่าแพ็กเก็ตจะถึง ไม่รับปากว่าจะถึงตามลำดับ ไม่รับปากว่าจะไม่ ถึงซ้ำสองครั้ง คำถามที่สองคือแล้วมันถึงจริงหรือเปล่า
UDP ตอบข้อแรกแล้วหยุด TCP ตอบทั้งสองข้อ และความต่างทุกอย่างที่เหลือระหว่างสอง โพรโทคอลนี้สืบกลับมาที่ประโยคนั้นได้หมด
นี่ไม่ใช่เรื่องว่าอันไหนดีกว่า TCP แพงกว่าเพราะมันสัญญามากกว่า และมีงาน จำนวนมากที่คำสัญญานั้นไม่คุ้มกับราคา
ถ้ายังไม่เคยดู เริ่มตรงนี้
เครื่องที่กำลังอ่านหน้านี้อยู่มีการเชื่อมต่อ TCP เปิดค้างอยู่หลายสิบรายการ และบอกสถานะของแต่ละรายการได้
$ netstat -an -p tcp | awk 'NR>2{print $NF}' | sort | uniq -c
67 ESTABLISHED
16 LISTEN
7 CLOSE_WAIT
2 TIME_WAIT
1 SYN_SENT
ชื่อสถานะเหล่านี้ไม่ใช่ศัพท์ของ macOS มันคือชื่อสถานะในเครื่องสถานะของ TCP ที่เขียนไว้ใน RFC ตั้งแต่ปี 1981 และยังใช้ชื่อเดิมทุกตัวจนถึงวันนี้
สองบรรทัดที่ควรสังเกตตั้งแต่ตอนนี้คือ CLOSE_WAIT เจ็ดรายการ กับ TIME_WAIT สองรายการ ทั้งสองอย่างจะกลับมาอธิบายตอนพูดถึงการปิดการเชื่อมต่อ และทั้งคู่บอก อะไรที่ต่างกันมาก
พอร์ต — สิ่งที่ทั้งคู่มีเหมือนกัน
คำตอบของคำถามแรกคือเลข 16 บิตสองตัว เรียกว่าพอร์ตต้นทางกับพอร์ตปลายทาง และมันอยู่ในสี่ไบต์แรกของทั้ง UDP และ TCP เหมือนกันเป๊ะ
16 บิตแปลว่ามีได้ 65,536 ค่า คือ 0 ถึง 65535 และถูกแบ่งเป็นสามช่วง
0 - 1023 well known
1024 - 49151 registered
49152 - 65535 dynamic หรือที่มักเรียกว่า ephemeral
ช่วงสุดท้ายคือช่วงที่เครื่องหยิบมาใช้เองตอนเปิดการเชื่อมต่อออกไป และเครื่องนี้ ยืนยันได้ตรง ๆ ว่าใช้ช่วงเดียวกันนั้นพอดี
$ sysctl net.inet.ip.portrange.first net.inet.ip.portrange.last
net.inet.ip.portrange.first: 49152
net.inet.ip.portrange.last: 65535
การเชื่อมต่อหนึ่งรายการถูกระบุด้วยสี่ค่า ไม่ใช่ค่าเดียว
source IP source port destination IP destination port
นั่นคือเหตุผลที่เปิดเว็บเดียวกันพร้อมกันสิบแท็บได้โดยไม่ปนกัน ทั้งสิบรายการมี ที่อยู่ปลายทางเดียวกัน พอร์ตปลายทางเดียวกัน ที่อยู่ต้นทางเดียวกัน ต่างกันแค่พอร์ตต้นทางคอลัมน์เดียว
UDP — แปดไบต์ และการไม่สัญญาอะไรเลย
RFC 768 ตีพิมพ์วันที่ 28 สิงหาคม 1980 เขียนโดย Jon Postel ยาวสามหน้า และยัง ไม่เคยถูกแทนที่
หัวของมันมีสี่ฟิลด์ ฟิลด์ละ 16 บิต รวมแปดไบต์
0 16 31
+---------+---------+---------+---------+
| source port | destination port |
+---------+---------+---------+---------+
| length | checksum |
+---------+---------+---------+---------+
ประโยคที่สำคัญที่สุดในเอกสารฉบับนั้นคือประโยคที่บอกว่ามันไม่ทำอะไร — "delivery and duplicate protection are not guaranteed" ไม่รับประกันว่าจะ ถึง และไม่รับประกันว่าจะไม่ถึงซ้ำ
สองฟิลด์หลังมีรายละเอียดที่น่าสนใจกว่าที่เห็น
length ดูเหมือนซ้ำซ้อน เพราะหัว IP ก็บอกความยาวอยู่แล้ว RFC ระบุว่าค่านี้ นับรวมหัวของตัวเองด้วย ค่าต่ำสุดจึงเป็น 8 ซึ่งคือดาต้าแกรมที่ไม่มีข้อมูลเลย
checksum คำนวณจากสิ่งที่ไม่ได้อยู่ในแพ็กเก็ต RFC บอกว่ามันคิดจาก pseudo header ซึ่งประกอบด้วยที่อยู่ต้นทาง ที่อยู่ปลายทาง หมายเลขโพรโทคอล และความยาว ของ UDP ทั้งหมดนี้ดึงมาจากหัว IP
นี่คือรอยรั่วของการแบ่งชั้นที่จงใจให้มีอยู่ ชั้นบนอ่านค่าจากชั้นล่างมาคำนวณ เพื่อให้จับกรณีที่แพ็กเก็ตถูกส่งผิดเครื่องได้ และมันคือเหตุผลที่ NAT ต้องคำนวณ checksum ใหม่ทุกครั้งที่แก้ที่อยู่
และ checksum ยังปิดได้ด้วย RFC เขียนไว้ว่า "An all zero transmitted checksum value means that the transmitter generated no checksum" ส่งเลขศูนย์ไปแปลว่า ไม่ได้คิดเลย
TCP — ยี่สิบไบต์ และคำสัญญาทั้งหมด
RFC ฉบับปัจจุบันของ TCP คือ RFC 9293 เดือนสิงหาคม 2022 ซึ่งแทนที่ RFC 793 จากปี 1981 พร้อมกับอีกหกฉบับ
ทุกฟิลด์
bytes field bits
0-1 source port 16
2-3 destination port 16
4-7 sequence number 32
8-11 acknowledgment number 32
12 data offset + reserved 4 + 4
13 control bits 8
14-15 window 16
16-17 checksum 16
18-19 urgent pointer 16
---
160 bits = 20 bytes
data offset นับเป็นคำ 32 บิต ไม่ใช่ไบต์ RFC นิยามว่ามันคือ "the number of 32-bit words in the TCP header" ฟิลด์นี้กว้าง 4 บิต ค่าสูงสุดจึงเป็น 15 คำ เท่ากับ 60 ไบต์ นั่นคือเพดานของหัว TCP ทั้งใบ และแปลว่าเหลือที่ให้ options มากที่สุด 40 ไบต์
เลข 40 ไบต์นั้นเป็นทรัพยากรที่หายากกว่าที่คนคิด และจะกลับมาเป็นประเด็นตอนพูดถึง window scaling
control bits มีแปดบิต และชื่อของมันคือคำที่เห็นบ่อยที่สุดเวลาดูทราฟฟิก
CWR ECE URG ACK PSH RST SYN FIN
หมายเลขลำดับนับไบต์ ไม่ใช่แพ็กเก็ต
นี่คือจุดที่เข้าใจผิดกันบ่อยที่สุดในหัว TCP ทั้งใบ
sequence number ไม่ได้นับว่านี่คือแพ็กเก็ตที่เท่าไร มันนับว่าไบต์แรกใน แพ็กเก็ตนี้คือไบต์ที่เท่าไรของสายข้อมูล ส่วน acknowledgment number บอกว่าไบต์ถัดไปที่อยากได้คือไบต์ที่เท่าไร
การนับเป็นไบต์คือสิ่งที่ทำให้ TCP แบ่งข้อมูลใหม่ระหว่างทางได้ ผู้ส่งจะรวมสอง แพ็กเก็ตเป็นหนึ่ง หรือแยกหนึ่งเป็นสองก็ได้ ตราบใดที่หมายเลขไบต์ยังต่อกันถูก
และมีข้อยกเว้นสองข้อที่ต้องจำ SYN กับ FIN กินหมายเลขลำดับไปคนละหนึ่งหมายเลข ทั้งที่ไม่ได้พาข้อมูลสักไบต์ RFC เขียนไว้ว่า SYN ถือว่าอยู่ก่อนไบต์ข้อมูล ไบต์แรก ส่วน FIN ถือว่าอยู่หลังไบต์ข้อมูลไบต์สุดท้าย
ข้อยกเว้นนี้คือคำอธิบายของเลข +1 ทุกตัวที่จะเห็นในหัวข้อถัดไป
จับมือสามทาง — ทำไมต้องสาม
-> SYN seq = x
<- SYN ACK seq = y ack = x+1
-> ACK seq = x+1 ack = y+1
คำถามที่ควรถามคือทำไมไม่สองครั้ง ในเมื่อฝั่งหนึ่งขอ อีกฝั่งตอบตกลง ก็น่าจะจบ
คำตอบอยู่ในตัวเลข มีเลขเริ่มต้นสองชุด ไม่ใช่ชุดเดียว ชุดของฝั่งที่เรียกคือ x และชุดของฝั่งที่รับคือ y ทั้งคู่เป็นเลขที่อีกฝ่ายไม่เคยเห็นมาก่อน และทั้งคู่ ต้องถูกตอบรับ
ข้อความที่สองทำสองหน้าที่พร้อมกัน คือตอบรับ x และประกาศ y ส่วนข้อความที่สาม ตอบรับ y งานสี่อย่างจึงบีบลงเหลือสามข้อความได้ แต่ไม่เหลือสอง
RFC 9293 อธิบายเหตุผลไว้ตรง ๆ ว่า "A 3WHS is necessary because sequence numbers are not tied to a global clock in the network" ไม่มีนาฬิกากลาง แต่ละเครื่องจึงเลือกเลขเริ่มต้นของตัวเอง และไม่มีทางเดาของอีกฝ่ายได้
ISN ต้องเดาไม่ได้ และนั่นคือเรื่องความปลอดภัย
ถ้าเลขเริ่มต้นเดาได้ ใครก็ตามที่เดาได้จะแทรกข้อมูลเข้าไปในการเชื่อมต่อที่มีอยู่ ได้โดยไม่ต้องเห็นทราฟฟิกเลย
RFC 9293 จึงกำหนดวิธีเลือกไว้
ISN = M + F(localip, localport, remoteip, remoteport, secretkey)
M คือตัวนับเวลาที่เดินทุกสี่ไมโครวินาที ส่วน F() ต้องเป็นฟังก์ชันสุ่มเทียม และ RFC ระบุเงื่อนไขไว้ชัดว่า "MUST NOT be computable from the outside"
สังเกตว่า F() รับค่าสี่ตัวที่ระบุการเชื่อมต่อเข้าไปด้วย ผลคือการเชื่อมต่อ คนละรายการได้เลขเริ่มต้นคนละชุด แม้จะเกิดขึ้นในวินาทีเดียวกัน
หน้าต่างสองบานที่คนสับสนกันบ่อยที่สุด
คำว่าหน้าต่างในบริบทของ TCP หมายถึงสองสิ่งที่คนละเรื่องกันสิ้นเชิง และการเรียก รวมกันคือที่มาของความสับสนจำนวนมาก
rwnd — ผู้รับรับไหวแค่ไหน
RFC 5681 นิยามไว้ว่า rwnd คือ "the most recently advertised receiver window" มันคือฟิลด์ window ในหัว TCP ที่อีกฝั่งประกาศออกมาตรง ๆ ว่าบัฟเฟอร์ของตัวเอง ยังว่างอยู่เท่าไร
นี่คือ flow control และมันป้องกันอย่างเดียวคือป้องกันไม่ให้ผู้ส่งท่วมผู้รับ
cwnd — เครือข่ายรับไหวแค่ไหน
cwnd คือ "a TCP state variable that limits the amount of data a TCP can send" และคำที่สำคัญที่สุดในนิยามนั้นคือ state variable
cwnd ไม่เคยปรากฏบนสายเลยสักครั้ง ไม่มีฟิลด์ไหนในหัว TCP ที่เก็บมัน มันเป็น ตัวเลขที่อยู่ในหน่วยความจำของผู้ส่งฝ่ายเดียว และเป็นผลของการเดาว่าเส้นทาง ระหว่างทางรับไหวแค่ไหน
นี่คือ congestion control และมันป้องกันคนละอย่างกับ flow control คือป้องกัน ไม่ให้ผู้ส่งท่วมเครือข่าย
ปริมาณที่ส่งได้จริงคือค่าที่น้อยกว่าของสองอย่าง RFC เขียนว่าผู้ส่งใช้ "the minimum of cwnd and rwnd"
การแยกสองอย่างนี้ออกจากกันมีผลจริงตอนแก้ปัญหา ทรูพุตที่ตันเพราะ rwnd แก้ด้วย การเพิ่มบัฟเฟอร์ ส่วนทรูพุตที่ตันเพราะ cwnd เพิ่มบัฟเฟอร์เท่าไรก็ไม่ช่วย
window scaling — 65535 ที่ไม่พอมาตั้งแต่ปี 1992
ฟิลด์ window กว้าง 16 บิต ค่าสูงสุดจึงเป็น 65,535 ไบต์
ตัวเลขนี้เคยพอ และเลิกพอไปนานแล้ว เหตุผลคำนวณได้ตรง ๆ
1 Gbit/s x 0.1 s RTT = 12.5 MB คือปริมาณที่เส้นทางพาได้พร้อมกัน
65,535 bytes / 0.1 s = 655,350 B/s คือที่หน้าต่างเดิมยอมให้ส่ง
= 5.24 Mbit/s ราว 0.5% ของลิงก์
บนเส้นทางกิกะบิตที่หน่วง 100 มิลลิวินาที หน้าต่างเดิมให้ได้ 5 เมกะบิตต่อวินาที เพราะผู้ส่งต้องหยุดรอตอบรับทุก 65,535 ไบต์ ไม่ว่าเส้นทางจะว่างแค่ไหน
RFC 7323 อธิบายไว้ว่า "For LFN paths where the bandwidth * delay product exceeds 64 KiB, the receive window limits the maximum throughput"
ทางแก้คือ options ที่ชื่อ window scale ซึ่งบอกจำนวนบิตที่ต้องเลื่อนค่าในฟิลด์
window field 16 bits -> 65,535 bytes
shift count max 14
scaled window 2^30 = 1 GiB
RFC ระบุเพดานไว้ว่า "the shift count MUST be limited to 14 (which allows windows of 2^30 = 1 GiB)"
และมีสองข้อที่ต้องรู้เพราะมันเป็นที่มาของปัญหาจริง
ข้อแรก options นี้ส่งได้เฉพาะตอนจับมือเท่านั้น RFC บอกว่ามันอยู่ใน SYN และ จะอยู่ใน SYN-ACK ก็ต่อเมื่อได้รับมาใน SYN ก่อน แปลว่าถ้าอุปกรณ์กลางทางตัดมันทิ้ง ตอนจับมือ การเชื่อมต่อนั้นจะติดอยู่ที่ 64 KiB ไปตลอดอายุ และไม่มีทางแก้ระหว่างทาง
ข้อสอง ถ้าฝั่งใดฝั่งหนึ่งไม่รองรับ ทั้งคู่กลับไปใช้ศูนย์ ไม่มีการต่อรอง ครึ่งทาง
เครื่องนี้ตั้งบัฟเฟอร์ไว้เกินเพดานเดิมไปแล้วสองเท่า
$ sysctl net.inet.tcp.sendspace net.inet.tcp.recvspace
net.inet.tcp.sendspace: 131072
net.inet.tcp.recvspace: 131072
131,072 ไบต์คือ 128 KiB ซึ่งมากกว่า 65,535 แปลว่าถ้าไม่มี window scaling เครื่องนี้ประกาศบัฟเฟอร์ของตัวเองออกไปให้ครบไม่ได้เลย
การควบคุมความคับคั่ง
TCP ไม่มีทางรู้ว่าเส้นทางระหว่างทางรับได้เท่าไร ไม่มีใครบอก และไม่มีฟิลด์ไหน ให้ถาม วิธีเดียวที่มันมีคือลองเพิ่มไปเรื่อย ๆ จนกว่าจะเจอสัญญาณว่าเกิน
และสัญญาณว่าเกินมีอย่างเดียวคือแพ็กเก็ตหาย
slow start ไม่ได้ช้า มันเริ่มจากน้อย
RFC 5681 บอกว่า "during slow start, a TCP increments cwnd by at most SMSS bytes for each ACK received" ซึ่งแปลว่า cwnd โตเป็นเท่าตัวทุกรอบไปกลับ ชื่อของมันหมายถึงจุดเริ่ม ไม่ได้หมายถึงอัตรา
จุดเริ่มนั้นเปลี่ยนมาแล้ว
RFC 5681 SMSS > 2190 IW = 2 * SMSS, max 2 segments
SMSS 1096 to 2190 IW = 3 * SMSS
SMSS <= 1095 IW = 4 * SMSS, max 4 segments
RFC 6928 IW = min(10*MSS, max(2*MSS, 14600))
RFC 6928 ปี 2013 ยกค่าเริ่มต้นขึ้นเป็นสิบ segment ซึ่งมีผลมากกับงานที่โอน ข้อมูลสั้น ๆ เพราะทั้งการโอนอาจจบก่อนที่ slow start จะทันขยายด้วยซ้ำ
เมื่อ cwnd โตเกิน ssthresh การเพิ่มเปลี่ยนจากทวีคูณเป็นเชิงเส้น
cwnd += SMSS * SMSS / cwnd
3 dup ACK — ลดครึ่งเดียว เพราะยังมีข่าวกลับมาอยู่
RFC 5681 กำหนดว่า "the fast retransmit algorithm uses the arrival of 3 duplicate ACKs as an indication that a segment has been lost"
เหตุผลที่ใช้เลขสามคือต้องแยกการสูญหายออกจากการมาไม่เรียงลำดับ แพ็กเก็ตที่แซงกัน เล็กน้อยทำให้เกิด ACK ซ้ำได้หนึ่งถึงสองใบ สามใบขึ้นไปจึงน่าเชื่อกว่า
ssthresh = max(FlightSize / 2, 2 * SMSS)
cwnd = ssthresh + 3 * SMSS
และนี่คือจุดที่กราฟลดครึ่งเดียวแทนที่จะกลับไปเริ่มใหม่ เพราะการที่ยังได้รับ ACK อยู่เป็นหลักฐานว่าแพ็กเก็ตอื่นยังเดินทางถึงปลายทางได้ เส้นทางยังไม่ตาย
RTO — เดาว่านานแค่ไหนถึงเรียกว่าหาย
ถ้าไม่มี ACK กลับมาเลย ต้องมีเวลาหนึ่งที่ตัดสินว่าหายแล้ว ค่านั้นคือ RTO และ RFC 6298 ให้สูตรไว้
first measurement
SRTT <- R
RTTVAR <- R/2
every measurement after
RTTVAR <- (1-beta)*RTTVAR + beta*|SRTT - R'|
SRTT <- (1-alpha)*SRTT + alpha*R'
always
RTO <- SRTT + max(G, K*RTTVAR)
alpha = 1/8 beta = 1/4 K = 4
สิ่งที่สูตรนี้ทำคือ ไม่ได้ดูแค่ค่าเฉลี่ย แต่ดูความผันผวนด้วย SRTT คือ ค่าเฉลี่ยแบบถ่วงน้ำหนัก ส่วน RTTVAR คือความผันผวน และ RTO ถูกตั้งไว้เหนือ ค่าเฉลี่ยเป็นระยะสี่เท่าของความผันผวน
เส้นทางที่หน่วงคงที่ 200 ms จึงได้ RTO ต่ำกว่าเส้นทางที่หน่วงแกว่งระหว่าง 50 ถึง 200 ms ทั้งที่ค่าเฉลี่ยอาจใกล้กัน ซึ่งถูกต้องแล้ว เพราะเส้นทางที่แกว่ง ต้องให้อภัยมากกว่า
RFC ยังกำหนดพื้นไว้ว่าถ้าคำนวณได้ต่ำกว่าหนึ่งวินาที "the RTO SHOULD be rounded up to 1 second" และเมื่อหมดเวลาแล้วยังไม่มา ให้ "set RTO <- RTO * 2"
การคูณสองทุกครั้งทำให้ช่วงเวลารอเป็นแบบนี้
1 2 4 8 16 32 64 ...
นี่คือคำอธิบายของอาการที่ทุกคนเคยเจอ — เครือข่ายหลุด แล้วโปรแกรมค้างอยู่นาน กว่าที่คาดมาก โดยไม่ยอมแจ้งข้อผิดพลาดสักที
ปิดการเชื่อมต่อ และ TIME_WAIT
การปิดใช้สี่ข้อความ ไม่ใช่สาม เพราะ แต่ละทิศทางปิดแยกกัน ฝั่งหนึ่งบอกว่าจะ ไม่ส่งอีกแล้วไม่ได้แปลว่าอีกฝั่งจะไม่ส่งอีก
ช่องว่างระหว่าง FIN ใบแรกกับ FIN ใบที่สองคือสถานะ CLOSE_WAIT ของฝั่งที่รับ และ ระบบปฏิบัติการไม่ได้เป็นคนตัดสินว่าจะอยู่ในสถานะนั้นนานแค่ไหน แอปพลิเคชัน เป็นคนตัดสิน เพราะมันจะออกจากสถานะนั้นเมื่อโปรแกรมสั่งปิดเท่านั้น
ส่วนฝั่งที่ปิดก่อนต้องอยู่ใน TIME_WAIT ต่ออีกระยะหนึ่ง RFC 9293 กำหนดว่า "MUST linger in the TIME-WAIT state for a time 2xMSL" และนิยาม MSL ไว้ว่า "For this specification the MSL is taken to be 2 minutes"
สองคูณสองนาทีเท่ากับสี่นาที นั่นคือสิ่งที่เอกสารบอก
เครื่องที่ใช้เขียนหน้านี้ไม่ได้ทำตามนั้น วิธีตรวจคือเปิดการเชื่อมต่อจริง ปิดจากฝั่งนี้ แล้วจับเวลาว่ารายการนั้นอยู่ในตารางนานแค่ไหน
0.0s local .56826 -> TIME_WAIT
31.0s local .56826 -> None
$ sysctl -n net.inet.tcp.msl
15000
สามสิบเอ็ดวินาที ไม่ใช่สี่นาที และค่า msl ที่เครื่องรายงานคือ 15000 ซึ่งเมื่อ เทียบกับผลที่วัดได้แล้วยืนยันว่าหน่วยเป็นมิลลิวินาที 15 วินาทีคูณสองได้ 30 ตรงกับที่วัด
การวัดสำคัญกว่าการอ่านหน่วยจากชื่อตัวแปร ถ้าเดาว่าเป็นวินาทีจะได้คำตอบผิดไป หกสิบเท่า
ทำไมต้องรออย่างนั้นด้วย เหตุผลคือแพ็กเก็ตเก่าของการเชื่อมต่อเดิมอาจยังหลงอยู่ใน เครือข่าย ถ้าปล่อยพอร์ตคู่เดิมให้ใช้ซ้ำทันที แพ็กเก็ตเก่านั้นจะโผล่มาปนกับ การเชื่อมต่อใหม่ที่ใช้สี่ค่าเดียวกัน TIME_WAIT คือการยอมเสียพอร์ตไปช่วงหนึ่ง เพื่อแลกกับการรับประกันว่าจะไม่มีของเก่าปน
head-of-line blocking — ราคาของคำสัญญา
TCP สัญญาว่าจะส่งข้อมูลให้แอปพลิเคชันตามลำดับ คำสัญญานั้นมีราคา และราคาของมัน ปรากฏชัดที่สุดตอนมีแพ็กเก็ตหายหนึ่งใบ
สมมติส่งหกก้อน ก้อนที่สามหาย ก้อนที่สี่ห้าหกมาถึงเรียบร้อย
ข้อมูลของสี่ห้าหกอยู่ในหน่วยความจำของเครื่องปลายทางแล้ว แต่แอปพลิเคชันได้รับ แค่หนึ่งกับสอง แล้วรอ เพราะถ้าส่งสี่ให้ไปก่อนสาม คำสัญญาเรื่องลำดับก็ผิด
สิ่งที่กั้นไม่ใช่เครือข่าย เครือข่ายทำงานเรียบร้อยแล้ว สิ่งที่กั้นคือคำสัญญา
RFC 9114 ซึ่งเป็นมาตรฐานของ HTTP/3 อธิบายผลของเรื่องนี้ไว้ตรง ๆ ว่า "a lost or reordered packet causes all active transactions to experience a stall regardless of whether that transaction was directly impacted by the lost packet" ทุกงานที่ทำอยู่หยุดหมด ไม่ว่างานนั้นจะเกี่ยวกับแพ็กเก็ตที่หายหรือไม่
นี่คือเหตุผลที่ QUIC เกิดขึ้น และเหตุผลที่มันเลือกวิ่งบน UDP RFC 9000 ระบุไว้ว่า "QUIC packets are carried in UDP datagrams to better facilitate deployment in existing systems and networks"
QUIC ไม่ได้ทิ้งความน่าเชื่อถือ มันย้ายความน่าเชื่อถือไปอยู่ที่ระดับสตรีม แทนที่จะอยู่ที่ระดับการเชื่อมต่อ สตรีมที่ไม่เกี่ยวกับแพ็กเก็ตที่หายจึงเดินต่อได้
เมื่อมันโกหก
"TCP รับประกันว่าข้อมูลจะถึง" ไม่ใช่ TCP รับประกันว่าถ้าข้อมูลถึง มันจะถึง ครบและถูกลำดับ ถ้าสายขาดจริง TCP บอกได้แค่ว่าล้มเหลว มันเปลี่ยนความล้มเหลวเงียบ ให้เป็นความล้มเหลวที่รายงานได้ ซึ่งต่างกันมาก แต่ไม่ใช่การรับประกันว่าจะสำเร็จ
"UDP เร็วกว่า TCP" ไม่ใช่ประโยคที่มีความหมาย ทั้งคู่วิ่งบนเส้นทางเดียวกัน ด้วยความเร็วเดียวกัน UDP ไม่มีการรอตอบรับและไม่มีการชะลอตัวเอง ซึ่งแปลว่ามัน ไม่รอ ไม่ได้แปลว่ามันเร็ว และงานที่ต้องการข้อมูลครบจะต้องไปสร้างการรอนั้นขึ้น มาเองในแอปพลิเคชันอยู่ดี
"ไม่มี ACK แปลว่าแพ็กเก็ตหาย" ไม่เสมอไป ACK อาจเป็นตัวที่หายเอง ผู้ส่งแยก สองกรณีนี้ไม่ออก และส่งซ้ำทั้งสองกรณีเหมือนกัน ผลคือปลายทางอาจได้ข้อมูลซ้ำ ซึ่งเป็นเหตุผลที่หมายเลขลำดับต้องทำหน้าที่กำจัดของซ้ำด้วย
"หน้าต่างใหญ่ขึ้นแล้วจะเร็วขึ้น" เฉพาะตอนที่ตัวจำกัดคือหน้าต่าง ถ้าตัวจำกัด คือ cwnd การเพิ่มบัฟเฟอร์ไม่ช่วยอะไรเลย และถ้าเพิ่มมากเกินไปในอุปกรณ์กลางทาง จะได้ปัญหาคนละอย่างคือคิวที่ยาวจนหน่วงเวลาพุ่ง
"TIME_WAIT เยอะ แปลว่ามีปัญหา" ไม่ใช่ TIME_WAIT คือร่องรอยของการปิดที่ เรียบร้อยจากฝั่งเรา สิ่งที่ควรกังวลกว่าคือ CLOSE_WAIT ที่ค้างอยู่นาน ๆ เพราะ นั่นแปลว่าโปรแกรมของเราลืมปิด
ตัวอย่างจริงจากงานจริง
กรณีที่ 1 — CLOSE_WAIT สะสมขึ้นเรื่อย ๆ จนเปิดการเชื่อมต่อใหม่ไม่ได้
เซิร์ฟเวอร์รับงานได้น้อยลงเรื่อย ๆ จนสุดท้ายไม่รับเลย รีสตาร์ตแล้วหายไปพักหนึ่ง แล้วกลับมาอีก
อ่านอย่างไร นับสถานะก่อนอย่างอื่น
$ netstat -an -p tcp | awk 'NR>2{print $NF}' | sort | uniq -c
4812 CLOSE_WAIT
23 ESTABLISHED
8 LISTEN
CLOSE_WAIT สี่พันกว่ารายการคือคำตอบทั้งหมด สถานะนี้แปลว่าอีกฝั่งส่ง FIN มาแล้ว เคอร์เนลตอบ ACK ไปแล้ว และกำลังรอให้แอปพลิเคชันฝั่งนี้สั่งปิด
เคอร์เนลจะไม่ปล่อยเองไม่ว่าจะรอนานแค่ไหน เพราะมันไม่มีสิทธิ์ตัดสินใจแทนโปรแกรม
นี่ไม่ใช่ปัญหาเครือข่าย เป็นบั๊กในโค้ดที่ลืมปิด socket ในบางเส้นทางการทำงาน มักเป็นเส้นทางที่เกิดข้อผิดพลาด ซึ่งเป็นเส้นทางที่คนทดสอบน้อยที่สุด
สิ่งที่ยังไม่ได้พิสูจน์ เรารู้ว่าโปรแกรมไม่ปิด แต่ยังไม่รู้ว่าเส้นทางไหนใน โค้ดที่ไม่ปิด ให้ดูว่า socket เหล่านั้นคุยกับปลายทางไหนเป็นหลัก แล้วไล่จากตรงนั้น
กรณีที่ 2 — ทรูพุตตันที่ตัวเลขเดิมทุกครั้ง ทั้งที่สายว่าง
โอนไฟล์ข้ามภูมิภาคได้ราว 5 Mbit/s ทุกครั้ง ไม่ว่าจะทำตอนไหน ทั้งที่ลิงก์เป็น กิกะบิตทั้งสองฝั่ง
อ่านอย่างไร ตัวเลขที่คงที่เกินไปคือลายเซ็นของขีดจำกัด ไม่ใช่ของความคับคั่ง ความคับคั่งจะแกว่ง
เอาทรูพุตกับ RTT มาคำนวณย้อนกลับหาหน้าต่างที่กำลังถูกใช้อยู่
5.24 Mbit/s / 8 = 655,000 B/s
655,000 B/s x 0.1 s = 65,500 bytes
ได้ 65,500 ซึ่งใกล้ 65,535 มากเกินกว่าจะเป็นเรื่องบังเอิญ การเชื่อมต่อนี้ ทำงานอยู่ที่เพดานของฟิลด์ window ที่ไม่ได้ขยาย
จากนั้นดูตอนจับมือว่ามีการตกลง window scale หรือไม่ ถ้าฝั่งหนึ่งส่ง options นี้ ไปแล้วอีกฝั่งไม่ตอบ ให้สงสัยอุปกรณ์กลางทางที่ตัด options ทิ้ง
สิ่งที่ยังไม่ได้พิสูจน์ การคำนวณบอกว่าหน้าต่างคือตัวจำกัด แต่ยังไม่ได้บอกว่า ใครเป็นคนทำให้ options หายไป ต้องเก็บแพ็กเก็ตตอนจับมือดูทีละจุดตามเส้นทาง
กรณีที่ 3 — โปรแกรมค้างนานกว่าที่ควรเมื่อปลายทางหายไป
ถอดสายที่ปลายทางแล้วโปรแกรมฝั่งนี้ไม่แจ้งข้อผิดพลาดสักที ค้างอยู่เป็นนาที
อ่านอย่างไร ดูช่วงเวลาระหว่างการส่งซ้ำแต่ละครั้ง
t = 0.00 ส่งครั้งแรก
t = 1.00 ส่งซ้ำ
t = 3.00 ส่งซ้ำ
t = 7.00 ส่งซ้ำ
t = 15.00 ส่งซ้ำ
t = 31.00 ส่งซ้ำ
ระยะห่างคือ 1 2 4 8 16 คือการคูณสองทุกครั้งตามที่ RFC 6298 กำหนด ไม่ใช่ ความผิดปกติ เป็นพฤติกรรมที่ถูกต้อง
การรวมเวลาทั้งหมดอธิบายว่าทำไมถึงนาน และอธิบายว่าทำไมการลด timeout ในโปรแกรม ถึงเป็นทางแก้ที่ตรงจุดกว่าการไปยุ่งกับ TCP
สิ่งที่ยังไม่ได้พิสูจน์ เห็นการคูณสองแล้วยืนยันได้ว่าไม่มี ACK กลับมา แต่ยัง แยกไม่ออกว่าแพ็กเก็ตขาไปหายหรือ ACK ขากลับหาย ต้องเก็บทราฟฟิกที่ปลายทางด้วย
กรณีที่ 4 — เปิดการเชื่อมต่อใหม่ไม่ได้ทั้งที่โหลดไม่สูง
เครื่องที่ยิงงานออกไปยังปลายทางเดียวกันถี่ ๆ เริ่มเปิดการเชื่อมต่อใหม่ไม่ได้ เป็นช่วง ๆ
อ่านอย่างไร คำนวณเพดานจากค่าที่เครื่องบอกเราเอง
65535 - 49152 + 1 = 16384 พอร์ตต้นทางที่หยิบได้
16384 / 30 s = 546 การเชื่อมต่อใหม่ต่อวินาที
ห้าร้อยสี่สิบหกต่อวินาทีคือเพดานจริง ไปยังปลายทางเดียวกันที่พอร์ตเดียวกัน เพราะพอร์ตที่ปิดไปแล้วยังถูกจองไว้ใน TIME_WAIT อีกสามสิบวินาที
สังเกตว่าเพดานนี้ต่อปลายทางหนึ่งราย ไม่ใช่ต่อเครื่อง เพราะการเชื่อมต่อระบุด้วย สี่ค่า ปลายทางคนละรายจึงใช้พอร์ตต้นทางเดียวกันซ้ำได้
สิ่งที่ยังไม่ได้พิสูจน์ เลข 546 คำนวณจากค่าของเครื่องนี้ เครื่องอื่นตั้งช่วง พอร์ตและ MSL ไม่เหมือนกัน ให้อ่านค่าจากเครื่องที่มีปัญหาจริงก่อนคำนวณ
อ้างอิง
มาตรฐานหลัก
- RFC 768 — User Datagram Protocol J. Postel, 28 สิงหาคม 1980 ยาวสามหน้า และเป็นที่มาของประโยคว่า delivery and duplicate protection are not guaranteed
- RFC 9293 — Transmission Control Protocol (TCP) สิงหาคม 2022 Standards Track แทนที่ RFC 793 และอีกหกฉบับ เป็นที่มาของโครงหัว เหตุผลของการจับมือสามทาง วิธีเลือก ISN และนิยามของ MSL
- RFC 793 กันยายน 1981 ฉบับดั้งเดิม ที่มาของชื่อสถานะทุกตัวที่
netstatยังพิมพ์ออกมาจนวันนี้
การควบคุมและการปรับแต่ง
- RFC 5681 — TCP Congestion Control กันยายน 2009 นิยามของ cwnd กับ rwnd กฎ slow start กฎ 3 duplicate ACK และสูตร ssthresh
- RFC 6298 — Computing TCP's Retransmission Timer สูตร SRTT RTTVAR และ RTO พร้อมค่าคงที่ alpha beta K และกฎการคูณสอง
- RFC 7323 — TCP Extensions for High Performance กันยายน 2014 window scaling เพดาน shift count ที่ 14 timestamps และ PAWS
- RFC 6928 — Increasing TCP's Initial Window เมษายน 2013 สถานะ Experimental ที่มาของ IW สิบ segment
สิ่งที่มาทีหลัง
- RFC 9000 — QUIC: A UDP-Based Multiplexed and Secure Transport พฤษภาคม 2021 และเหตุผลที่เลือกวิ่งบน UDP
- RFC 9114 — HTTP/3 มิถุนายน 2022 คำอธิบายผลของ head-of-line blocking ที่ชัดที่สุดที่หาได้
ของจริงบนเครื่อง
netstat -an -p tcpชื่อสถานะทั้งหมดมาจาก RFC โดยตรงsysctl net.inet.ip.portrange.firstและ.lastช่วงพอร์ตที่หยิบมาใช้เองsysctl net.inet.tcp.sendspaceและ.recvspaceบัฟเฟอร์ที่ตั้งไว้ 128 KiB ซึ่งเกินเพดานของฟิลด์ window ที่ไม่ขยายไปแล้วสองเท่าsysctl -n net.inet.tcp.mslให้ค่า 15000 และการวัด TIME_WAIT จริงได้ 31 วินาที ซึ่งยืนยันว่าหน่วยเป็นมิลลิวินาที ไม่ใช่วินาที
อ่านต่อในชุดนี้
- Internet Protocol — ชั้นที่อยู่ใต้ลงไป และคำสัญญาที่มันไม่ให้
- NAT — สิ่งที่แก้พอร์ตต้นทางในหัวที่หน้านี้อธิบายไว้
- DNS — งานที่ใช้ UDP เป็นหลักและถอยไปใช้ TCP เมื่อคำตอบใหญ่เกิน