网络部署相关 | 青训营笔记

162 阅读11分钟

网络与部署相关笔记

这是我参与「第三届青训营 -后端场」笔记创作活动的的第5篇笔记

(一)概述

分为四个方面:

  • 网络接入协议
  • 网络传输协议
  • 网络优化
  • 网络稳定

(二)基础概念

1. 网络接入协议、网络传输协议

1.1 网络接入协议/概念

  • MAC地址:网卡的物理地址
  • 路由协议:路由器相关,怎么找目的 ip 的网络地址
  • ARP协议:根据 ip 找 MAC 地址
  • IP协议:ip 地址
  • NAT

1.2 网络传输协议

  • DNS:根据域名找ip地址
  • UDP:用户数据报
  • TCP:传输连接协议
  • HTTP:超文本传输协议
  • HTTPS:安全版 HTTP
  • HTTP2.0:基于长连接的流水线传输,有队头堵塞
  • QUIC:基于UDP的传输协议

2. 课程涉及软件/环境安装搭建

抓包软件

建议在Linux安装tcpdump软件(apt/yum命令安装,参考相关博客。如无法安装,可以下载源码安装www.tcpdump.org/)

安装wireshark(根据你的主机选择安装版本www.wireshark.org/,如果是Linux主机…

3. 思考题

  • 按照 TCP/IP 的模型示意图,能否画出数据包的拆包/封包?
  • TCP 的拥塞算法有哪些?课件建议熟练掌握。
  • 建议熟练掌握 socket 编程。
  • 建议阅读 golang/java 等高级编程下的 net 相关库的源码。
  • 了解 Linux kernel 的网络包从收到包到用户态,从用户态发包到网卡整个流程?

(三)笔记详情

3.1 抖音网络是怎么交互的?

思考:为了让抖音工作,网络需要哪些交互?

  • 网络接入
  • 网络传输

3.2 网络接入

3.2.1 网络拓扑的整体认知

  • 移动设备 ----> 路由器、4G、5G ----> 运营商网络 ----> 具体服务的服务器机房

image.png

3.2.2 路由发包原理

(1)同网段

配置网段即可默认添加静态路由。获取对端MAC直接发包。

image.png

image.png

(2)跨网段

配置网关路由。获取网关MAC地址发包。

image.png

(3)动态路由:BGP/OSPF 等,路由表在动态变化
(4)路由是网状的不一定是对称的

image.png

(5)路由工作在 IP 层,但其内部涉及的一些协议与传输层有关
(6)路由是改的IP地址吗?

不是,是改下一跳的MAC地址传输过程中源IP和目标IP始终不变,只有源MAC和目标MAC在改。

(7)发包代码

image.png

3.2.3 ARP协议(找下一跳MAC)

  • 逻辑同网段才能发送ARP
  • ARP广播/应答:ARP请求广播,ARP响应单播
  • 免费ARP:主动广播告知MAC地址,新开了一个主机时(判断主机冲突)
  • ARP代理:虚拟网络/伪造MAC地址

3.2.4 IP协议

  • 唯一标识,互联网通用
  • Mac地址不能代替IP地址吗?Mac协议是二层的协议,但是二层有很多协议,无法进行统一,因此在二层之上的三层使用ip协议进行统一;Mac地址难记,会重复,一个网卡一个,同一主机可能会有多个
  • IPv4: 互联网终端节点的唯一标识
  • IPv6: 不仅仅是IP地址长度的增加

3.2.5 NAT(当IPv4不够用时)

  • NAT上网:家用路由器,将同网段下的设备的ip地址和端口同时改变,对外实现统一,对内实现区分
  • NAT出网机房内网主机上外网
  • NAT原理:注意不仅仅是源地址变换,源端口/校验和/SEQ等都会变化

3.3 网络传输

3.3.1 数据包

本质上是一段内存,里面存储的内存是有序的,一般是按照TCP/IP的多层协议去封装。拆包/封包都是按照协议去写内存/读内存。

image.png

image.png

image.png

3.3.2 DNS递归迭代

image.png

image.png

3.3.3 UDP(常用作DNS的传输协议)

image.png

  • 协议简单,想发什么包,就分配一个UDP的头,把payload 里面塞数据发出去就好。

  • 需要考虑可靠性的场景,使用复杂,UDP用好很难

    • 发包每次发多少?怎么避免分片?(MTU最大传输单元,有限制,需要将数据进行切分)
    • 怎么知道没丢包?
    • 怎么权衡传输效率和质量?
  • 怎么保证协议可靠?

3.3.4 TCP

(1)三次握手

确认传输的起始序列号/MSS(最大分段大小,最大报文长度)/Option字段,建立连接

image.png

(2)TCP连接:是一个虚拟的概念,本质上两倍维持一段内存,记录连接状态,就是session
(3)TCP传输:理解sequence number(Seq)/acknowledge number(Ack)

image.png

  • Timewait: 两倍的(MSL)最大报文存活时间/报文段寿命,保证连接正常关闭,防止前一次的应答报文(ACK)丢失,允许客户端响应报文有一次丢失机会;避免当前连接中的旧报文对相同四元组连接造成影响,确保当前连接存活的报文都消失。
(4)丢包重传:丢包怎么感知并重传?快速重传发生在什么时候?

快速重传,超时重传,ACK 机制保证可靠性

(5)滑动窗口
(6)流量控制

3.3.5 HTTP

  • HTTP 比 TCP 好在哪里:方便,HTTP 只是多加了一层规矩,HTTP 依然是 TCP ,只是这个规矩让用户更清晰、更简洁。

  • HTTP 1.1 的优化:长连接是重点

  • HTTPS

    • HTTPS的产生背景:加密/可靠/防劫持
    • SSL/TLS握手:非对称加密/对称加密、 image.png

3.4 网络提速

3.4.1 HTTP2.0

多路复用:可以在一个TCP 连接上跑多个 HTTP请求,但依然有队头阻塞

image.png

image.png

虽然说是可以在同一个 TCP 连接通道里同时进行多个 HTTP传输,但在内部实现上仍是采用串行传输方式,只是切换太快。这时,一旦发生丢包,就会进行重传,在重新接收到期望的数据包之前要一直等待,发生全部重传,容易造成队头阻塞

3.4.2 QUIC / HTTP3.0(解决队头堵塞问题)

(1)优势:弱网传输,解决队头阻塞问题,组装思想
(2)为什么在用户态实现?

内核的更新迭代频率较低,不好推广,需要根据不同的操作系统分别进行实现,很麻烦。

(3)为什么用UDP?

TCP的队头阻塞问题不好解决,推倒重来&复用所有操作系统基本都支持的底层协议

image.png

3.4.3 网络路径优化

(1)数据中心建设

有边缘机房(靠近客户端)/汇聚机房/中心机房(服务器端)

image.png

(2)多运营商接入

同运营商内部访问,避免跨运营商的流量

image.png

(3)CDN 静态缓存系统

静态资源(图片视频) ,路径优化,边缘机房的建设,优先访问边缘机房,缓存命中视频/图片等静态内容

image.png

(4)DSA 动态加速系统

动态(API)(播放、评论接口) 路径优化,分四层/七层动态加速,核心在于利用可控节点做路径探测和规划

e.g. 根据每个机房的时延,规划最优路径

image.png

3.5 网络稳定

3.5.1 容灾

image.png

3.5.2 网络容灾的具体案例

(1)机房专线故障

专线连接各个机房的网络物理路径。内部机房不走外部 Internet,而是拉一根线之类的物理连接(交换机、集线器…),将机房直接相连,避免丢包,绕路

外网:跟 Internet 连接

环路容灾,转线走不通就走外网,避免某条专线故障导致机房孤岛问题

image.png

(2)单机房接入节点故障

DNS 容灾,摘除故障的节点 —— 字节GTM系统 :如果 A 机房挂了,直接在 DNS 过程中将节点 A 的 ip 剔除,替换成备用节点 B的 ip,同时,在替换成 B 之前完成故障感知,确保 B 的容量是足够承载从 A 流过来的访问量的,避免出现雪崩

image.png

(3)云控容灾

云端交互,服务器/云上下发命令到终端 —— 字节TNC系统

image.png

(4)cache容灾

源站不可用,降级到之前的缓存内容 —— 字节 TLB/ByteCDN 等系统的容灾建设

image.png

3.5.3 故障排查

image.png

(1)故障明确:出现什么故障?沟通是前提
  • 什么业务?什么接口故障?
  • 故障体现在哪里?
  • 访问其他目标是否正常?
  • 是否是修改导致的异常?
(2)故障止损:
  • 要在第一时间做(灾备预案的建设) ,先止损再排查
  • 如何止损?组件没有容灾,但是系统有没有?;降级;
(3)分段排查
  • 客户端排查

    • 客户端访问其他服务没问题吗?
    • 其他客户端访问目标服务没问题吗?
  • 服务端排查

    • 服务端监控/指标都正常吗?
    • 手动访问一下正常吗?
    • 分组件排查
  • 中间链路排查

    • 服务端跟客户端确保没有问题
    • 中间网络设备有没有问题?(交换机/路由器/网关LB)
    • 旁路的DNS有没有问题
(4)网络故障排查常用命令
  • dig 查询 DNS 问题
  • ping / telnet / nmap 查询三层 / 四层连通性
  • Traceroute 排查中间链路,测试是否有丢包现象
  • iptabels,查查有没有防火墙
  • tcpdump 抓包

3.5.4 故障排查的具体案例

(1)服务端配置异常(健康检查异常)

image.png

(2)客户端某个例异常(客户端自己配置错误)

image.png

(3)外部运营商故障

image.png

(4)复杂故障的排查:需要抓包,具体问题具体分析

某 APP 故障 ----> 后端服务器反馈服务正常 ----> 网络转发设备异常 ----> 抓包 ----> 路由不对称

(四)课后思考

课后作业1- UDP socket 实现 ack,感知丢包重传

作业要求:

  1. 学会 UDP socket 编程
  2. 先从简单的 ack 学习,客户端等待 ack 再发包
  3. 什么时候客户端认为是丢包?
  4. 重传怎么考虑效率?
  5. 能不能不阻塞只穿丢掉的中间的段?
TCP socket 编程:

img

 //服务器端
 import java.io.IOException;
 import java.net.InetAddress;
 import java.net.ServerSocket;
 import java.net.Socket;
 ​
 public class Server {
     public static void main(String[] args){
         //创建一个服务器端的Socket,即ServerSocket,绑定需要监听的端口
         try {
             ServerSocket serverSocket = new ServerSocket(8888);
             Socket socket = null;
             //记录连接过服务器端客户端数量
             int count = 0;
             System.out.println("服务器即将启动,等待客户端的连接");
             while(true){//循环监听新的客户端的连接
                 //调用accept()方法监听,等待客户端的连接以获取Socket实例
                 socket = serverSocket.accept();
                 //创建新线程
                 Thread thread = new Thread(new ServerThread(socket));
                 thread.start();
                 count++;
                 System.out.println("服务器端被连接过的次数:"+count);
                 
                 InetAddress address = socket.getInetAddress();
                 System.out.println("当前客户端IP为:"+address.getHostAddress());
             }
             //服务器端的连接不用关闭。
         } catch (IOException e) {
             // TODO Auto-generated catch block
             e.printStackTrace();
         }
     }
 }
 //客户端
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.net.Socket;
 import java.net.UnknownHostException;
 ​
 ​
 public class Client1 {
     public static void main(String[] args) {
         try{
             Socket socket = new Socket("localhost",8888);
             OutputStream os = socket.getOutputStream();
             PrintWriter pw = new PrintWriter(os);
             pw.write("用户名:jinxueling;密码:123");
             pw.flush();
             socket.shutdownOutput();
             
             InputStream is = socket.getInputStream();
             InputStreamReader isr = new InputStreamReader(is,"UTF-8");
             BufferedReader br = new BufferedReader(isr);
             String data = null;
             while((data = br.readLine())!=null){
                 System.out.println("我是客户端,服务器端响应的数据为:"+data);
             }
               socket.close();
         }catch (UnknownHostException e) {
             e.printStackTrace();
         }catch(IOException e){
             e.printStackTrace();
         } 
     }
 }
UDP Socket编程

img

 //线程工具类
 package UDPSocket;
 ​
 import java.net.DatagramPacket;
 import java.net.DatagramSocket;
 import java.net.InetAddress;
 ​
 public class UDPThread implements Runnable{
     
     DatagramSocket socket = null;
     DatagramPacket packet = null;
     public UDPThread(DatagramSocket socket,DatagramPacket packet){
         this.socket = socket;
         this.packet = packet;
     }
     @Override
     public void run() {
         String info = null;
         InetAddress address = null;
         int port = 8800;
         byte[] data2 = null;
         DatagramPacket packet2 = null;
         try{
             //打印当前请求socket客户端的请求数据和信息。
             info = new String(packet.getData(),0,packet.getLength());
             System.out.println("我是服务器,客户端说:"+info);
             //封装数据包,响应给当前socket实例的客户端
             address = packet.getAddress();
             port = packet.getPort();
             data2 = "我在响应你".getBytes();
             packet2 = new DatagramPacket(data2, data2.length,address,port);
             socket.send(packet2);
         }catch(Exception e){
             e.printStackTrace();
         }
     }
 }
 //服务端
 package UDPSocket;
 ​
 import java.io.IOException;
 import java.net.DatagramPacket;
 import java.net.DatagramSocket;
 import java.net.InetAddress;
 import java.net.SocketException;
 ​
 public class UDPServer {
     public static void main(String[] args) throws IOException {
         try {
             //创建指定端口的DatagramSocket
             DatagramSocket socket = new DatagramSocket(8800);
             //声明数据报
             DatagramPacket packet = null;
             //声明字节数组
             byte[] data = null;
             //服务器响应的连接计数
             int count = 0;
             System.out.println("服务器启动,等待发送数据");
             //等待客户端连接
             while(true){
                 //初始化字节数组容量,指定接收的数据包的大小
                 data = new byte[1024];
                 //初始化数据包
                 packet = new DatagramPacket(data,data.length);
                 //等待接收来自服务端的数据包
                 socket.receive(packet);
                 //到达这一步,socket.receive方法停止阻塞了,说明有客户端在请求了
                 //给该客户端创建一个独立的线程,并根据接收到的包,给予响应。
                 Thread thread = new Thread(new UDPThread(socket,packet));
                 thread.start();
                 count++;
                 System.out.println("服务器端被连接过的次数:"+count);
                 //打印当前的客户端socket的ip
                 InetAddress address = packet.getAddress();
                 System.out.println("当前客户端的IP为:"+address.getHostAddress());
             }
         } catch (SocketException e) {
             e.printStackTrace();
         }
     }
 }
 //客户端
 package UDPSocket;
 ​
 import java.io.IOException;
 import java.net.DatagramPacket;
 import java.net.DatagramSocket;
 import java.net.InetAddress;
 ​
 public class UDPClient {
     public static void main(String[] args) throws IOException {
         //定义服务器的地址,端口号,数据
         InetAddress address = InetAddress.getByName("localhost");
         int port = 8800;
         byte[] data = "用户名:admin;密码:123".getBytes();
         //创建数据报
         DatagramPacket packet = new DatagramPacket(data,data.length,address,port);
         //创建DatagramSocket,实现数据发送和接收
         DatagramSocket socket = new DatagramSocket();
         //向服务器发送数据报
         socket.send(packet);
         
         //接收服务器 响应数据
         byte[] data2 = new byte[1024];
         DatagramPacket packet2 = new DatagramPacket(data2,data2.length);
         socket.receive(packet2);
         String info = new String(data2,0,packet2.getLength());
         System.out.println("我是客户端,服务器说:"+info);
         socket.close();
     }
 }