分类: 未分类

  • Python第三天

    在 Python 中,文件操作是非常常见的任务。Python 提供了内置的函数来处理文件的读写操作。

    文件的类型

    1. 文本文件:包含可读的字符(如 .txt、.csv)。一般使用 UTF-8 编码,可以使用文本编辑器查看。
    2. 二进制文件:包含非文本数据(如图像、音频、视频文件,后缀如 .jpg、.png、.mp3)。以原始==字节格式==存储。需要使用专用软件查看,Bytes形式存储。

    ==注意==:后缀名不会影响文件类型 .md | .txt |.csv 都是文本文件

    文件操作的过程

    1. 打开文件
    2. 读写文件
      • 读文件:将文件内容读入内存
      • 写文件:将内存内容写入文件
    3. 关闭文件

    操作方法

    文件路径

    • 绝对路径:从根路径开始描述文件的位置,例如:F:技术文件课件-笔记课件修订Python1.Python基础语法,具有唯一性,不会出错,不管写在哪里都可以准确的找到文件的位置
    • 相对路径:相对当前位置进行文件定位,容易出错,需要对路径比较熟悉

    ==注意==:会相对路径就可,到时候linux会重新搞

    txt.path=Path(__file__).parent  #获取当前运行程序脚本文件的上一级文件夹路径,方便后续定位文件夹里的其他脚本文件,如当前为.py,可以找所在文件夹的其他文件.txt
    
    # 后径路径拼接..
    #Path(__file__).parent   相当于文件夹路径被定死可以在任何程序文件运行

    with open(‘ 文件名’,’方式’,’编码 ‘)

    使用 with 语句可以自动管理文件的打开和关闭,with 语句打开后自动关闭文件

    with open('example.txt', 'r') as file:        # 获取file文件对象
        content = file.read()

    ==注意==:在windows系统自动关闭后文件不会被占用,可删除或重命名

    以不同模式打开

    访问方式 说明
    r 以只读方式打开文件。文件的指针将会放在文件的开头,这是默认模式。如果文件不存在,报错
    a 以追加方式打开文件。如果该文件已存在,文件==指针==将会放在==文件的结尾==。如果文件不存在,创建新文件进行写入
    r+b 以二进制读写方式打开。文件指针在开头,可读可写,文件必须存在
    a+b 以二进制读写方式打开。文件指针在末尾,追加写入,可读全文,不存在则创建

    ==注意==:对于==二进制文件binary==,只能使用 b 模式,表示以 bytes 方式操作(所有文件本质上最终都是字节存储,使用这种模式无需考虑文本文件的字符编码),读取到的内容是字节类型,写入时也需要提供字节类型,不能指定编码 r 和 a 以 字符串Str形式读写

    文件编码

    由操作系统打开文件,那么如果我们没有指定编码,那么打开文件的默认编码取决于操作系统,操作系统会用自己的默认编码去打开文件,在 Windows 下是 gbk,在 Linux 下是 utf-8。

    with open('example.txt','r',encoding='utf-8') as file:

    ==注意==:file是==文件对象==:代表打开的文件

    读取文件

    • read(size):读取指定大小的内容,如果没有指定,读取全部内容。
    • readline():读取一行。
    • readlines():读取所有行并返回一个列表。

    ==注意==:size 标准为 8192(8 k b) 读写磁盘大文件 64kb

    示例:

    with open('example.txt','r',encoding='utf-8') as file:
    
    content = file.read()          # 读取全部内容
    line = file.readline()      # 读取一行
    lines = file.readlines()      # 读取所有行
    
    print(content)
    print('-----------')
    print(line)
    print('-----------')
    print(lines)
    
    # 思考?
    # 为什么后面两个print读取不到东西呢?

    ==注意==:光标读到哪就放到哪不会重置开头,读完返回空字符串 ‘ ‘ 或 空列表 [ ]

    文件指针

    用 seek(0) 来操作指针,重置光标位置

    语法:

    file.seek(0)
    • 括号里

      (可选):指定光标基准位置,可以取以下值:

      • 0:从文件开头开始计算(默认值)
      • 1:从当前位置开始计算
      • 2:从文件末尾开始计算

    ​

    示例:

    with open('example.txt','r',encoding='utf-8') as file:
    
    content = file.read()          # 读取全部内容
    file.seek(0)                # 把光标移到到文件的开头
    line = file.readline()      # 读取一行
    file.seek(0)                # 再次把光标移动到文件的开头
    lines = file.readlines()      # 读取所有行
    
    print(content)
    print('-----------')
    print(line)
    print('-----------')
    print(lines)

    按行读取文件内容

    • read() 默认会把文件的所有内容一次性读取到内存
    • readline() 可以一次读取一行内容
    # 方式一、 通过循环按行读取文件所有内容
    file1 = open("example.txt")
    i = 1
    while True:
        text1 = file1.readline().strip()
        if text1:
            print("这是第%s行内容" % i)
            i += 1
            print(text1)
        else:
            break
    
    file1.close()
    
    file2 = open("example.txt")
    
    #  通过for循环遍历按行读取文件所有内容
    for i in file2.readlines():
        print(i.strip())
    
    file2.close()

    写入内容

    用 file.write( ) 写入内容

    语法:

    file.write( )

    示例:日记记录

    # 日记程序
    # 以追加模式打开日记文件
    with open('diary.txt', 'a',encoding='utf-8') as file:
    
    # 获取用户输入的日记内容
    content = input("请输入今天的日记:")
    
    # 将日记内容写入文件
    file.write(content + "n")
    print("日记已保存!")
    

    案例:简单的备份程序

    将一个文本文件的内容复制到另一个文件,用于简单的备份。

    # 简单备份小程序
    'a.txt'
    'b.txt'
    
    with open('a.txt','r',encoding='utf-8') as src:
        content = src.read()
    
    with open('b.txt', 'a',encoding='utf-8') as dest:  # 以追加方式打开文件'b.txt'
        dest.write(content)
    
    print(f"备份成功!'a.txt' 的内容已复制到 'b.txt'")
    

    Json 文件读取

    json本质上是文本格式,为” ……… “,引号里是python的所有脚本.

    JSON类型 Python类型
    object dict
    array list
    string str
    number(整形)int int
    number(浮点型)float float
    true True
    false False
    null None

    转换方法

    作用 输出
    json.dumps() 将 Python 对象序列化为 JSON 字符串 obj → str
    json.dump() 将 Python 对象序列化为Json格式并写入文件( obj, filename,encoding=utf-8) → 无返回值
    json.loads() 将 JSON 字符串反序列化为 Python 对象 str → obj
    json.load() 从文件读取并反序列化为 Python 对象 (filename) → obj

    ==注意==:区别在于数据来源或去向:带s的是处理字符串(在内存中操作),没有写入文件 ;没有s的是处理文件对象,读写文件内容

    dump把写的代码转为json格式写到文件里,load是从 Json文件读取到 python 程序中.

    序列化和反序列化

    序列化:从 Python 到 JSON

    反序列化: 从 JSON 到 Python

    文件读写操作

    1. 把data以json格式写入到文件
    data = {"result": "success", "code": 200}
    
    with open("output.json", "a", encoding="utf-8") as f:
        json.dump(data, f)
    1. 把json文件里的东西读取到python里,去掉 ” “
    with open("output.json", "r", encoding="utf-8") as f:
        data = json.load(f)

    JSON 与 bytes 的 转换

    JSON 本质文本格式为str,转换成字节才可在二进制里运行

    1. 编码与解码
    • Json 到 bytes:先 Python 对象 变成 JSON 字符串(dumps),再通过.encode( )编码成字节形式
    • bytes 到 Json:先将字节通过decode( )解码成字符串,再 loads( )解码成Python对象

    ==注意==:一切都是围绕str到bytes的编解码

    转换路线

    方向 步骤
    Python对象到bytes obj到JSON字符串再到bytes
    bytes到Python对象 bytes到JSON字符串再到obj
    import json
    
    data = {"action": "login", "user": "张三", "ts": 1234567890}
    
    # === 对象 → bytes ===
    json_bytes = json.dumps(data, ensure_ascii=False).encode("utf-8")
    print(json_bytes)   # b'{"action":"login","user":"张三","ts":1234567890}'
    
    # === bytes → 对象 ===
    recovered = json.loads(json_bytes.decode("utf-8"))
    print(recovered["user"])  # 张三

    读写二进制Json文件

    手动编码后写入,读取时先解码

    1. 写入二进制 Json文件
    data = {"sensor": "temp", "value": 23.5}
    
    with open("data.bin", "wb") as f:
        # 将对象转为 bytes 后写入
        f.write(json.dumps(data, ensure_ascii=False).encode("utf-8"))
    1. 读取二进制文件
    with open("data.bin", "rb") as f:
        raw_bytes = f.read()   #  先把读取的内容写到内存上来,不然没有目标 
        obj = json.loads(raw_bytes.decode("utf-8"))

    Base64:把图片,音视频等二进制转入JSON存储

    json不能存储真正二进制内容,==须先用Base64编码成字符串==,字符串可安全放入JSON字符串字段里

    典型JSON结构示例:

    {
      "fileName": "photo.jpg",
      "mimeType": "image/jpeg",
      "data": "/9j/4AAQSkZJRgABAQEBLAEsAAD..."
    }

    图片,音视频都是二进制形式

    ==注意==:Base64编码返回永远是字节,所以必须.decode()去掉b得到字符串;b64decode解码自动处理字符串去掉字符串标识,把字符串还原为二进制得到原本的b,b64decode解码直接把字符串传进去

    · 编码:base64.b64encode(二进制数据) → 返回 bytes(例如 b’data’) 如果你想要 str(文本字符串),需要再调用 .decode(‘utf-8’)。 所以确实“不是直接转的,需要多一步 decode”。 · 解码:base64.b64decode(字符串) → 返回原始 bytes。 它可以接受 str 参数(如 ‘….’),不需要你先 encode 成 bytes。 所以解码时“可以直接转”。

    你的理解完全正确,而且区分了编码和解码的不同处理方式。简洁准确地概括了 Python base64 模块的行为👍

    ru

  • python第二天

    列表(List)

    列表是一个有序的可变集合,可以存储不同类型的元素。列表相比于字符串,不仅可以储存不同的数据类型,而且可以储存大量数据。而且列表是有序的,有索引值,可切片,方便取值。

    ###  定义列表
    fruits = ["apple", "banana", "cherry"]
    print(fruits)
    
    # Output:
    ['apple', 'banana', 'cherry']

    增加元素

    # 1. 按照索引的位置去增加
    fruits.insert(1, "kiwi")
    
    # 2. 在最末尾添加
    fruits.append("orange")
    
    # 3. 用迭代的方式去添加
    fruits.extend(['a','b','c'])        
    # 可以添加列表、字典等,把其中的元素依次添加到末尾,而append会把括号当成一个整体添加到末尾
    
    # Output:
    ['apple', 'kiwi', 'banana', 'cherry', 'orange', 'a', 'b', 'c']

    删除元素

    # 1. 按照索引位置去删除
    fruits.pop()        # 默认是删除最后一个,可以加上索引。并且有返回值,返回值为删除的元素
    fruits.pop(1)
    
    想得到返回值:poped_fruits=fruits.pop(1)    可以pop(index)得到任意单元素变量
    
    
     # 2. 清空列表
    fruits.clear()
    
    # output: [] 

    修改元素

    # 1. 按照索引位置 ,修改某个元素的值
    fruits = ["apple", "banana", "cherry"]
    fruits[1] = "orange"
    print(fruits)
    
    # Output:
    ['apple', 'orange', 'cherry']
    
    # 2. 按照切片范围修改
    fruits[0:2] = ['kiwi','orange']
    print(fruits)
    
    # Output:
    ['kiwi', 'orange', 'cherry']

    查找元素

    # 1. 用变量名.index("元素名") ,返回元素的索引
    fruits = ["apple", "banana", "cherry"]
    index_of_apple = fruits.index("apple")
    print(index_of_apple)
    
    # Output:
    0
    
    # 2. 用变量名.count("元素名")方法,返回元素出现的次数
    count_of_cherry = fruits.count("cherry")
    print(count_of_cherry)
    
    # Output:
    1

    列表的切片

    切片:切出==新的列表== ,按照索引位置切片

    li = [1,2,4,5,4,2,4]
    sub_li = li[2:5]
    print(sub_li)
    
    # Output:
    [4, 5, 4]

    排序和反序方法

    # 1. 对列表进行排序
    li.sort( )
    print(li)
    
    # 按照首字母顺序和数字大小排列
    
    #2. 将列表中的元素反向存放
    li.reverse()
    print (li)
    
    # Output:
    
    [1, 2, 2, 4, 4, 4, 5]
    [5, 4, 4, 4, 2, 2, 1]

    元组(Tuple)

    有序的==不可变集合==, 即==只读列表==,里面==数据不可被修改==

    定义元组

    tuple = (1,2,3,'a','b','c')
    print(tuple)
    print(tuple[1])
    
    # 可以删除元素吗?
    tuple.pop()
    
    # 报错里面的元素不可-增或删-,pop()方法用不了

    可变元组

    tuple 其实==不可变的是地址空间==,如果地址空间里存的是List,Dict,那么==列表或字典里的元素可增改==,列表或字典不可删除

    tuple = (1, 2, 3, [1, 4, 7])
    print(tuple)
    tuple[3][2] = 100
    print(tuple)
    
    # Output:
    (1, 2, 3, [1, 4, 7])
    (1, 2, 3, [1, 4, 100])

    <img src=”https://li-zheng-blog-2026.oss-cn-shanghai.aliyuncs.com/blog/%E5%85%83%E7%BB%84%E7%A9%BA%E9%97%B4%E5%9C%B0%E5%9D%80.png” style=”zoom: 80%;” />

    字典(Dict)

    字典是 python 中唯一的==映射==类型,采用键值对(key-value)的形式存储数据。python 对 key 进行哈希函数运算,根据计算的结果决定 value 的存储地址,所以字典是==无序存储==的,且key必须是可哈希的。可哈希表示==key== 必须是不可变类型,如:==数字、字符串、元组==。

    不过在 Python 3.6 版本以后,字典会保留插入顺序,变成有序数据结构。

    而且键是具有唯一性的,==每个键只能出现一次==,但是值没有限制,在一个字典中可以出现多个相同的值

    定义字典

    dic = {'name':'nls','age':18,'job':'teacher'}
    print(dic)
    print(dic['name'])
    print(dic['age'])
    print(dic['job'])
    
    # Output:
    {'name': 'nls', 'age': 18, 'job': 'teacher'}
    nls
    18
    teacher

    ==注意==:键不可变且只出现一次不能有相同的key,但value不唯一

    dic = {'name':'nls','age':18,'job':'teacher'}
    
    # 1. 直接通过键值对来增加
    dic['hobby'] = 'study'        # 如果键已经存在,那么就会替换原来的值
    
    print(dic)
    
    # Output: {'name': 'nls', 'age': 18, 'job': 'teacher', 'hobby': 'study'}
    

    删除键值

    dic = {'name':'nls','age':18,'job':'teacher'}
    
    # 1. 删除指定的键,并且返回对应的值
    name = dic.pop('job')
    hobby = dic.pop('hobby','None')        # 可以在后面加上一个异常处理,如果key不存在,就输出后面的内容
    print(dic)
    print(name)
    print(hobby)
    
    # Output:
    {'name': 'nls', 'age': 18}
    teacher
    None
    
    # 2. 删除最后插入的键值对
    dic_pop = dic.popitem()
    print(dic_pop)
    
    #返回一个元组:('job','teacher')
    
    
    # 3. 清空字典
    dic.clear()
    print(dic)
    
    # Output: {}
    

    修改键值

    dic = {'name':'nls','age':18,'job':'teacher'}
    dic['age'] = 25
    
    print(dic)
    
    # Output:
    {'name': 'nls', 'age': 25, 'job': 'teacher'}

    查找键值

    dic = {'name':'nls','age':18,'job':'teacher'}
    # 1. 直接通过键名获取,如果不存在会报错
    value = dic['name']
    print(value)
    
    
    # 2. IN关键字,存在返回True,反之False
    exists = "name" in dic
    print(exists)
    
    # Output:
    True

    其他操作

    dic = {'name':'nls','age':18,"phone":['1888888888','0511-10101010']}
    # 字典里面的value也可以是容器类型的数据,比如列表,字典等等...但是key必须是不可变的
    print(dic)
    
    # 1. 对键和值进行迭代操作
    for i in dic.items():
        # 将键和值作为元组列出
        print(i)
    
    
    # Output:
    ('name', 'nls')
    ('age', 18)
    ('phone', ['1888888888', '0511-10101010'])
    
    
    # 2. 使用keys()和values()方法获取键值
    keys = dic.keys()
    print(keys)            # dict_keys(['name', 'age', 'phone']) 
    value = dic.values()
    print(value)          # dict_values(['nls', 18, ['1888888888', '0511-10101010']])
    
    # 想将其转化为列表
    key_list=list(keys)
    val_list=list(value)
    
    # ['name','age','phone']
    # ['nls',18,['1888888888','0511-10101010']]
    
    

    ==注意==:直接取键或值的列表print( list( dict.keys( ) ) )

    ​ print( list( dict.values( ) ) )

    ['name','age','phone']
    ['nls',18,['1888888888','0511-10101010']]

    集合(Set):无序,去重,{ }

    集合是无序的,不重复,确定性的数据集合,它里面的==元素是不可变==,但是集合本身是可变的(所以集合做不了字典的键)。以下是集合最重要的两点:

    • ==去重==,把一个列表变成集合,就==自动去重==了。set( ) 括号里放List,Dict,Tuple,Str,
    • 关系测试,测试两组数据之前的交集、差集、并集等关系。

    add 增加元素

    set1 = {1,2,3,'a','b','c'}
    # 1. 使用add()方法为集合增加元素,因为无序所以set没有append( )方法
    set1.add('Apple')
    print(set1)
    
    # Output:
    {'a', 1, 2, 3, 'c', 'Apple', 'b'}
    
    # 2. 使用update()方法迭代的去增加
    set1.update('Helloworld')
    # update接收的参数应该是可迭代的数据类型,比如字符串、元组、列表、集合、字典。这些都可以向集合中添加元素,但是整型、浮点型不可以
    set1.update([4,5,6])
    print(set1)
    
    # Output:
    {1, 2, 3, 4, 5, 6,'a','b','c','H','e','l','o','w','r','d'}

    ==注意==:[4,5,6]里面元素是一个一个加进去的

    ​ 'Helloworld'这个字符串里的单个字符是一个一个加进去的,并且去重了,集合是不重复的

    remove 删除元素

    set1 = {1,2,3,'a','b','c'}
    # 1. 使用remove()方法删除元素,因为无序所以不能pop(index)按索引删除元素
    set1.remove('a')
    
    print(set1)
    
    # Output:
    {1, 2, 3, 'c', 'b'}
    

    关系测试

    交集

    取出两个集合共有的元素

    set1 = {1,2,3,4,5}
    set2 = {3,4,5,6,7}
    
    print(set1 & set2)     #  &=and
    
    # Output:
    {3, 4, 5}

    反交集:交集的补集

    set1 = {1,2,3,4,5}
    set2 = {3,4,5,6,7}
    
    print(set1 ^ set2)    # 全集-交集
    
    # Output:
    {1, 2, 6, 7}

    并集

    set1 = {1,2,3,4,5}
    set2 = {3,4,5,6,7}
    
    print(set1 | set2)  #  | = set1 + set2
    
    # Output:
    {1, 2, 3, 4, 5, 6, 7}

    差集

    第一个集合去除二个集合的共同元素

    set1 = {1,2,3,4,5}
    set2 = {3,4,5,6,7}
    
    print(set1 - set2)
    
    # Output:
    {1, 2}

    子集与超集

    当一个集合的所有元素都在另一个集合里,则称这个集合是另一个集合的子集,另一个集合是这个集合的超集

    set1 = {1,2,3}
    set2 = {1,2,3,4,5,6}
    
    print(set1 < set2)  #  set1 是子集, set2是超集
    
    
    print(set2 > set1)

    frozenset不可变集合

    set1 = {1,2,3,4,5,6}
    
    set2 = frozenset(set1)
    print(set2)
    
    set2.add(7) # 不可以修改,会报错,frozenset冻结了集合
    
    # Output:
    # 报错

    ==数据结构总结==

    数据结构 字符串str ” “ 列表list [ ] 元组 ( ) 字典Dict{ } 集合 Set { }
    描述 字符的序列 有序的,可变 [ ] 有序的,不可变,( ) 无序的,键值对,{ } 无序的,可变集合set( )
    特性 不可变 可变,支持重复 不可变,支持重复 可变,键唯一 无序,可变,==不支持重复==

    数据结构是计算机存储、组织数据的方式。选择合适的数据结构,可以显著提升算法的效率。简单来说,它定义了数据如何存放以及数据之间有何关系。


    一:为什么要学习数据结构? 它的核心价值在于权衡与优化。同一个问题可以用不同数据结构解决,但效率天差地别。

    提高效率:良好的数据结构能显著提升算法的时间(如查找、排序)和空间效率。例如,在100万条记录中查找一个用户,哈希表(O(1))比数组遍历(O(n))快数十万倍。

    组织复杂数据:现实世界的数据关系复杂(如组织架构、社交网络),需要树、图等结构来建模。

    支持特定操作:有些问题需要特殊的存取方式,比如撤销操作天然适合“后进先出”的栈结构。

    二:如何选择合适的数据结构? 没有完美的数据结构,只有最合适的。选择时主要考虑以下三点:

    核心操作:你的程序主要进行查找、插入、删除还是排序?

    数据量级:数据是固定的几十个,还是百万级动态增长?

    数据关系:数据间是一对一的线性关系,还是一对多的层次、多对多的网状关系?

    一个经典的选择场景:实现一个网页的“最近浏览记录”功能,要求快速访问最近5条记录,且新记录要淘汰最旧的。这个需求恰好符合队列的先进先出特性,并用固定大小的数组实现即可。

    流程控制

    判断语句

    语法:

    if 条件:
        满足条件后要执行的代码

    ==注意==:只有条件== ==True==时才可执行 if 板块体内缩进代码

    多分支判断

    if 条件:
        满足条件执行代码
    elif 条件:
        上面的条件不满足就走这个
    elif 条件:
        上面的条件不满足就走这个
    elif 条件:
        上面的条件不满足就走这个    
    else:
        上面所有的条件不满足就走这段
    
    
    # 成绩判断程序
    # 提示用户输入成绩
    score = int(input("请输入成绩(0-100):"))
    
    # 多分支判断成绩
    if score >= 90 and score <= 100:
        print("优秀")
    elif score >= 60 and score < 90:
        print("良好")
    elif score >= 0 and score < 60:
        print("不及格")
    else:
        print("输入错误,请输入0到100之间的成绩。")

    ==注意==:只有一个分支会运行

    循环语句 – while

    语法:

    while 条件:
         循环体
    
    # 循环条件可以直接是True/False或者1/0,也可以是某个语句...

    如果条件为真,那么循环体则执行 ; 如果条件为假,那么循环体不执行

    示例:猜数字小游戏

    print('猜数字游戏开始')
    num = 54
    while True:
        guess = int(input("您猜数字是什么?(输入0-100的数字):"))
        if guess < num:
            print("您猜小了")
            continue
        elif guess > num:
            print("您猜大了")
            continue
        break
    
    print("您猜对了!")
    
    # Output:
    猜数字游戏开始
    您猜数字是什么?(输入0-100的数字):10
    您猜小了
    您猜数字是什么?(输入0-100的数字):50
    您猜小了
    您猜数字是什么?(输入0-100的数字):60
    您猜大了
    您猜数字是什么?(输入0-100的数字):54
    您猜对了!

    ==注意==: continue 会跳过 单分支条件判断,break会跳出循环体while终止循环

    循环终止语句

    break

    用于完全结束一个循环,跳出循环体执行循环后面的语句

    continue

    和 break 有点类似,区别在于 continue 只是终止本次循环,接着还执行后面的循环,break 则完全终止循环

    while …… else

    while 后面的 else 作用是指,当 while 循环正常执行完,中间没有被 break 中止的话,就会执行 else 后面的语句

    • 语法:
    while condition:
        # 循环体
        if some_condition:
            break
    else:
        # 循环正常结束时执行的代码

    循环语句for

    for 循环:用户按照顺序循环可迭代对象的内容

    语法:

    for variable in iterable:
        # 循环体
        # 执行的代码

    遍历列表

    fruits = ["apple", "banana", "cherry"]
    for fruit in fruits:
        print(fruit)
    
    # Output:
    apple
    banana
    cherry

    遍历字符串

    word = "helloworld"
    for letter in word:
        print(letter)
    
    # Output:
    h
    e
    l
    l
    o
    w
    o
    r
    l
    d

    enumerate

    enumerate:枚举,对于一个可迭代的(iterable)/可遍历的对象(如列表、字符串),enumerate 将其组成一个索引序列,利用它可以同时获得==索引==和==值==。

    li = ['甲','乙','丙','丁']
    for i in enumerate(li):
        print(i)
    
    for index,value in enumerate(li):
        print(index,value)
    
    for index,value in enumerate(li,100):      #  从哪个数字开始索引
        print(index,value)
    
    # Output:
    (0, '甲')
    (1, '乙')
    (2, '丙')
    (3, '丁')
    0 甲
    1 乙
    2 丙
    3 丁
    100 甲
    101 乙
    102 丙
    103 丁

    range

    指定范围,生成指定数字

    for i in range(1,10):
        print(i)
    
    for i in range(1,10,2):  # 步长,取奇数
        print(i)

    小游戏案例

    import random
    # random产生随机值或者从给定值中随机选择
    
    # 可选择的选项
    options = ["石头", "剪子", "布"]
    
    print("欢迎来到石头剪子布游戏!")
    print("请从以下选项中选择:")
    for i, option in enumerate(options):
        print(f"{i + 1}. {option}")
    
    # 玩家选择
    player_choice = int(input("请输入你的选择(1-3):")) - 1
    
    # 计算机随机选择
    computer_choice = random.randint(0, 2)
    
    print(f"n你选择了:{options[player_choice]}")
    print(f"计算机选择了:{options[computer_choice]}")
    
    # 判断胜负
    if player_choice == computer_choice:
        print("平局!")
    elif (player_choice == 0 and computer_choice == 1) or 
         (player_choice == 1 and computer_choice == 2) or 
         (player_choice == 2 and computer_choice == 0):
        print("你赢了!🎉")
    else:
        print("你输了!😢")
  • python基础第一天

    1. 注释

    • # 这是一条注释,解释做什么
    1. 变量:

      变量就是把某些值或者运行的结果临时保存在内存中,以便后续调用


    2.变量命名约定

    python变量命名规则

    • 根据习惯选择一种,选择下划线命名法
    
    - 下划线命名法:将多个单词连接在一起,使用下划线 `_` 分隔每个单词,所有字母小写,常用于变量和模块名的命名。
    ```python
    first_str = 'Hello'
    second_str = 'Python'

    3.变量赋值

    将某个数据赋值给某个变量存储

    a = "变量1"
    b = "变量2"
    print(f"{a} {b}")

    ==注意==:不存在常量,约定的也可更改。

    4.用户交互

    解释器在执行该代码的时候,如果遇到 input() 就会出现阻塞,程序暂停,等待用户输入。当用户回车以后,程序才会继续向下执行

    name = input('请输出你的姓名:')
    print(name)
    
    # 注意:通过input函数输入进来的内容一律会被当成字符串处理
    a = input('请输入一个数字:')
    # a = int(input('请输入一个数字:'))  # 类型转化
    print(type(a))  
    print(a > 0)

    数据类型

    整数(int)

    • 描述:表示没有小数部分的数字,可以是正数、负数或零。

    • 特性:

      • Python 3 中的 int 类型是无限大小的。这意味着你可以创建非常大的整数,而不必担心溢出(overflow)的问题。
      • Python 会根据整数的大小自动调整内存使用,因此可以存储任意大小的整数,只要计算机的内存允许。
      • 支持基本的算术运算,如加、减、乘、除等
      a = 10
      b = -5
      c = 0
      print(a + b)
      # Output: 5
      print(type(a))  # 可以使用 type() 内置函数来查看某个变量或值的数据类型
      # Output:
      <class 'int'>

      字符串(str)

    • 描述:字符串是字符的序列,可以通过单引号或双引号定义。

    • 特性:

      • 字符串是不可变的(immutable),不能更改其内容。
      • 支持切片、连接、重复等操作。
      • 可以使用转义字符处理特殊字符。
      • 在 Python 中, 加了引号的字符都被认为是字符串!
    Greeting = "Welcome to 英格科技"
    Name = "牛老师"
    print(Greeting + " " + Name)    # 字符串可以通过 + 号进行拼接
    
    # Output:
    Welcome to 英格科技 牛老师

    ==字符串的索引和切片==

    字符串的索引:

    索引:每个字符在字符串中都有一个唯一的索引,索引从 0 开始。

    负索引:可以使用负数索引从字符串的末尾开始访问字符,-1 表示最后一个字符,-2 表示倒数第二个字符,以此类推。

    ==注意==:最好不要用负索引,就按正常顺序就可,留着大脑学难的

    s = "Hello, World!"
    
    # 正索引
    print(s[0])  # 输出: H
    print(s[7])  # 输出: W
    
    # 负索引
    print(s[-1])  # 输出: !
    print(s[-5])  # 输出: o

    字符串的切片

    切片:可以通过指定起始索引和结束索引来提取字符串2的子串。切片的语法为 s[start:end],包含起始索引的字符,但不包含结束索引的字符。

    步长:可以指定步长来控制切片的间隔,语法为 s[start:end:step]。如果不指定步长,默认为 1

    s = "Hello, World!"
    
    # 基本切片
    print(s[0:5])  # 输出: Hello
    print(s[7:12])  # 输出: World
    
    # 省略起始或结束索引
    print(s[:5])  # 输出: Hello (从开始到索引 4)
    print(s[7:])  # 输出: World! (从索引 7 到结束)
    
    # 使用负索引切片
    print(s[-6:-1])  # 输出: World
    
    # 步长切片
    print(s[::2])  # 输出: Hlo ol! (每隔一个字符)
    print(s[::-1])  # 输出: !dlroW ,olleH (反转字符串)
    print(s[:])   #输出:"Hello,world!"

    常用方法

    words = "beautiful is better than ugly."
    
    print(words.title())        # 每个单词的首字母大写
    
    print(words.find('sdj', 1, 10))          # 返回索引,找不到返回 -1
    
    # split 以指定字符分割,形成列表
    ret = words.split(' ',number)    #['beautiful', 'is', 'better', 'than', 'ugly.']
                              #被引号里的空格字符分割形成相应的元素,可指定分割几次默认从左往右
    print(ret)
    ret = words.rsplit(' ', 2)           # 指定分割次数,从右往左
    print(ret)
    
    # strip 操作
    a = '****asdasdasd********'
    print(a.strip('*'))   # 去除两端指定字符
    print(a.lstrip('*'))  # 去除左侧指定字符
    print(a.rstrip('*'))  # 去除右侧指定字符
    #多用于面向对象时,得到信息,删除不必要的信息
    

    ==总结==:

    字符串切片能让你从原字符串中提取出一段连续的子串,而不会修改原字符串。它是处理文本时最常用、最高效的操作之一。

    基本能力(以 Python 为例)

    语法:字符串[开始索引:结束索引:步长]

    • 提取子串:取中间某几个字符 s = "Hello, World!" s[7:12] → "World"
    • 省略索引: s[:5] → 从头到索引5之前 → "Hello" s[7:] → 从索引7到末尾 → "World!"
    • 负数索引:从右向左数 s[-6:-1] → "World"
    • 复制整个字符串:s[:] → "Hello, World!"
    • 反转字符串:s[::-1] → "!dlroW ,olleH"
    • 每隔几个字符取一个:s[::2] → "Hlo ol"

    实际能解决什么问题?

    1. 截取文件名后缀 filename[-3:] 取最后3个字符(假设后缀固定长度,更严谨用 split)。
    2. 去除固定前缀/后缀 url[7:] 去掉 http://。
    3. 解析固定格式数据 身份证号 id_card[6:14] 提取出生日期。
    4. 快速判断字符串是否以某子串开头/结尾(虽然也有 startswith,但切片可见一斑)
    5. 文本对齐/截断 text[:20] + "..." 显示前20个字符后加省略号。

    重要规则

    • 切片是左闭右开 [开始, 结束):包含开始索引,不包含结束索引。
    • 索引越界不会报错,只会尽可能返回已有部分(比直接索引安全)。
    • 步长为负数时,从右向左取,常用于反转或逆序提取。

    布尔类型(bool)

    • 描述:布尔类型只有两个值:True 和 False。
    • 特性:
      • 通常用于条件判断和逻辑运算。
      • 布尔值在逻辑表达式中可以进行运算。

    布尔类型很简单,就两个值 ,一个 True(真),一个 False(假), 主要用于逻辑判断

    a = 3
    b = 5
    print(a < b, a > b , a != b)
    
    # Output:
    True False True

    基本运算符

    算数运算符

    用于执行基本的数学运算

    运算符 描述 示例
    + 加法,返回两个数的和 3 + 5 → 8
    - 减法,返回第一个数减去第二个数的结果 10 - 4 → 6
    * 乘法,返回两个数的乘积 2 * 3 → 6
    / 除法,返回第一个数除以第二个数的结果,结果为浮点数 7 / 2 → 3.5
    // 地板除法,返回两个数相除的整数部分 7 // 2 → 3
    % 取模,返回第一个数除以第二个数的余数 7 % 2 → 1
    ** 幂运算,返回第一个数的第二个数次幂 2 ** 3 → 8

    关系运算符

    用于比较两个值的关系

    运算符 描述 示例
    == 等于,判断两个值是否相等 5 == 5 → True
    != 不等于,判断两个值是否不相等 5 != 3 → True
    > 大于,判断左侧值是否大于右侧值 5 > 3 → True
    < 小于,判断左侧值是否小于右侧值 5 < 3 → False
    >= 大于等于,判断左侧值是否大于或等于右侧值 5 >= 5 → True
    <= 小于等于,判断左侧值是否小于或等于右侧值 3 <= 5 → True

    身份运算符

    用于比较两个对象的内存地址

    运算符 描述 示例
    is 判断两个对象是否为同一对象 x is y
    is not 判断两个对象是否不是同一对象 x is not y

    成员运算符

    用于检查值是否在序列中

    运算符 描述 示例
    in 判断元素是否在序列中 3 in [1, 2, 3] → True
    not in 判断元素是否不在序列中 4 not in [1, 2, 3] → True

    ​

  • Nginx反向代理和负载均衡

    • Nginx反向代理配置实例

    image-20260826182127273

    • web03服务器,配置一个网站,监听在8080
    [root@web03 ~]# cd /etc/nginx/conf.d/
    [root@web03 conf.d]# vim web.conf
    server {
        listen 8080;
        server_name localhost;
    
        location / {
            root /code/8080;
            index index.html;
            allow all;
        }
    }
    [root@web03 conf.d]# nginx -t
    nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
    nginx: configuration file /etc/nginx/nginx.conf test is successful
    [root@web03 conf.d]# systemctl restart nginx
    [root@web03 ~]# mkdir -p /code/8080
    [root@web03 ~]# echo "listening 8080 ..." > /code/8080/index.html
    • proxy代理服务,配置监听80端口,使能够通过代理服务器访问到后端的192.168.226.134的8080端口站点内容
    [root@proxy ~]# cd /etc/nginx/conf.d/
    [root@proxy conf.d]# vim proxy_web_node1.conf
    server {
        listen 80;
        server_name proxy.test.com;
    
        location / {
            proxy_pass http://192.168.226.134:8080;
        }
    }
    [root@proxy conf.d]# nginx -t
    [root@proxy conf.d]# systemctl restart nginx
    • 存在的问题,通过抓包可以看到客户端是使用域名对网站进行访问的,但是代理却是使用的IP地址加端口号
    • 当访问80端口的时候,没有域名的情况下,默认会去找排在最上面的那个配置文件。
    • 所以我们需要解决这个问题,保留住最开始的请求头部信息。

    架构说明

    text

    客户端服务器 (192.168.226.128)
        ↓ 访问 http://proxy.test.com (域名)
        ↓ hosts: 192.168.226.132 proxy.test.com
    代理服务器 (192.168.226.132) 
        ↓ 监听 80端口
        ↓ proxy_pass http://192.168.226.134:8080
        ↓ 转发请求(保留原始头信息)
    后端Web服务器 (192.168.226.134)
        ↓ 监听 8080端口
        ↓ 返回 "listening 8080 ..."

    实践

    1. 后端web服务器

    image-20260826184055480

    1. 中间代理服务器proxy_server

    image-20260826201921312

    1. 后端业务处理服务器

    image-20260826201959024

    1. 客户端服务器(192.168.226.128)

    角色:访问发起方 / 用户端

    职责:

    • 发起 HTTP 请求,目标域名是 http://proxy.test.com
    • 通过本地 /etc/hosts 文件,将域名 proxy.test.com 解析为代理服务器的 IP 192.168.226.132
    • 不参与任何 Nginx 配置,纯粹是”用户”角色

    简单理解:就是”用户”或”浏览器”


    2. 代理服务器(192.168.226.132)

    角色:反向代理 / 入口网关

    职责:

    • 监听 80 端口,接收来自客户端的所有请求
    • 域名匹配:识别 server_name proxy.test.com,处理该域名的请求
    • 转发请求:通过 proxy_pass 将请求转发给后端服务器 http://192.168.226.134:8080
    • 保留原始请求信息:通过 proxy_set_header 将客户端的真实IP、域名等信息传递给后端,让后端知道请求来自谁
    • 负载均衡(可选):如果后端有多个服务器,可以在此做分发

    简单理解:就是”前台”或”门卫”,所有请求先到它这里,再由它转交给真正干活的人

    关键配置:

    nginx

    server {
        listen 80;
        server_name proxy.test.com;
    
        location / {
            proxy_pass http://192.168.226.134:8080;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }

    3. 后端 Web 服务器(192.168.226.134)

    角色:真正的业务处理 / 应用服务器

    职责:

    • 监听 8080 端口(避免和代理服务器的 80 端口冲突)
    • 提供业务内容:返回静态页面、处理业务逻辑、读取数据库等
    • 接收代理转发来的请求,处理完成后将结果返回给代理服务器,由代理服务器再返回给客户端
    • 可以配置多个,组成集群,由代理服务器做负载均衡

    简单理解:就是”后台员工”或”真正干活的”,负责提供实际内容

    关键配置:

    nginx

    server {
        listen 8080;
        server_name localhost;
    
        root /code/8080;
        index index.html;
    }

    架构原理

    1. 代理服务器(132)标准配置

    这是你作为入口网关的完整配置,每一条都有具体作用:

    nginx

    server {
        # 1. 监听端口
        listen 80;                     # 监听所有网卡的80端口(不要写127.0.0.1,否则外部访问不了)
        server_name proxy.test.com;    # 匹配客户端请求的域名
    
        location / {
            # 2. 转发目标(指向后端)
            proxy_pass http://192.168.226.134:8080;
    
            # 3. 【核心】保留原始域名(解决多站点路由问题)
            proxy_set_header Host $host;
    
            # 4. 【核心】传递真实客户端IP(解决日志溯源问题)
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    
            # 5. 【核心】启用HTTP/1.1长连接(解决高并发性能问题)
            proxy_http_version 1.1;
            proxy_set_header Connection "";   # 清除默认的"close",启用Keep-Alive
    
            # 6. 【可选】传递原始协议(如果后端需要区分http/https)
            proxy_set_header X-Forwarded-Proto $scheme;
    
            # 7. 【可选】超时控制(防止后端响应慢导致连接断开)
            proxy_connect_timeout 60s;
            proxy_send_timeout 60s;
            proxy_read_timeout 60s;
    
            # 8. 【可选】缓冲控制(平衡性能与实时性)
            proxy_buffering on;
            proxy_buffer_size 4k;
            proxy_buffers 8 4k;
        }
    }

    2. 后端服务器(134)标准配置

    这是真正提供内容的服务器配置:

    nginx

    server {
        listen 8080;                   # 监听8080端口(与代理的proxy_pass一致)
        server_name localhost;         # 这里实际接收的是代理传来的Host头
    
        root /code/8080;               # 网站根目录
        index index.html index.htm;    # 默认首页文件
    
        location / {
            # 允许所有访问(如果内网环境)
            allow all;
        }
    
        access_log /var/log/nginx/access.log main;
        error_log /var/log/nginx/error.log;
    }

    3. 客户端(128)配置

    客户端只需要做好域名解析即可,无需配置Nginx:

    bash

    # 编辑 /etc/hosts,添加:
    192.168.226.132  proxy.test.com

    4. 完整请求流程(配图理解)

    text

    客户端(128)
        ↓ 访问 http://proxy.test.com
        ↓ /etc/hosts 解析 → 192.168.226.132
        ↓
    代理服务器(132)
        ↓ listen 80; server_name proxy.test.com;
        ↓ proxy_pass → http://192.168.226.134:8080
        ↓ 传递:Host, X-Real-IP, X-Forwarded-For
        ↓ 使用:HTTP/1.1 长连接
        ↓
    后端服务器(134)
        ↓ listen 8080;
        ↓ 根据 Host: proxy.test.com 匹配站点
        ↓ 记录日志:$http_x_forwarded_for = 真实客户端IP
        ↓
    返回 "This is the server3 web" ✅

    Nginx 反向代理高并发配置总结

    核心配置

    nginx

    proxy_http_version 1.1;
    proxy_set_header Connection "";

    为什么要加?

    对比项 默认(HTTP/1.0) 加配置后(HTTP/1.1)
    连接方式 短连接(一次请求一建一断) 长连接(Keep-Alive,复用连接)
    高并发后果 端口快速耗尽,大量 TIME_WAIT,新请求进不来 几十个固定连接反复使用,稳定高效
    资源消耗 耗 CPU(频繁握手/挥手) 耗少量内存,节省 CPU 和时间

    为什么要配两行?

    • proxy_http_version 1.1; → 告诉 Nginx 用 HTTP/1.1 协议向后端发请求
    • proxy_set_header Connection ""; → 删掉默认的 Connection: close,让连接保持不断开

    缺一不可:只写第一行不写第二行,后端收到 Connection: close,长连接不会生效。

    什么时候必须加?

    • ✅ 生产环境 / 高并发场景 → 必须加

    image-20260827112822610


    Nginx负载均衡

    Nginx负载均衡基本概述

    为什么要使用负载均衡

    • 当我们的Web服务器直接面向用户,往往要承载大量并发请求,单台服务器难以负荷,我使用多台Web服务器组成集群,前端使用Nginx负载均衡,将请求分散的打到我们的后端服务器集群中,实现负载的分发。那么会大大提升系统的吞吐率、请求性能、高容灾

    image-20260827141033300

    四层和七层负载均衡

    一、Web服务 → 为什么选Nginx(七层)?

    场景举例

    • 用户访问 http://proxy.test.com
    • 需要根据不同的URL转发到不同的后端(/api → API服务器,/static → 静态资源服务器)
    • 需要保持用户的会话(同一个用户的请求始终落到同一台服务器)
    • 需要记录客户端的真实IP

    为什么Nginx能做这些?

    Nginx是七层负载均衡,它会完整地接收客户端的HTTP请求,拆开”信封”,读取里面的内容:

    text

    GET /api/user HTTP/1.1
    Host: proxy.test.com        ← Nginx能读到这个域名
    Cookie: session_id=abc123   ← Nginx能读到这个Cookie
    User-Agent: curl/7.76.1

    基于这些信息,Nginx可以做:

    功能 怎么实现 为什么四层做不了
    域名路由 根据 Host: proxy.test.com 转发到对应的后端 四层看不到域名
    URL路径匹配 location /api/ 转发到API服务器,location /static/ 转发到静态服务器 四层看不到URL
    会话保持 根据Cookie里的 session_id 把同一用户固定到同一台后端 四层看不到Cookie
    Header改写 添加 X-Forwarded-For 传递真实IP 四层无法修改HTTP头
    重定向/rewrite 根据业务规则重写URL 四层无法操作应用层数据

    选型结论

    只要你的业务逻辑和HTTP协议相关(域名、URL、Cookie、Header),就必须用七层负载均衡(Nginx)。


    二、数据库/端口转发 → 为什么选LVS(四层)?

    场景举例

    • MySQL数据库集群:多个MySQL实例,需要把请求分发给不同的数据库服务器
    • Redis缓存集群:多个Redis节点,需要做读写分离
    • 游戏服务器:基于TCP长连接,转发玩家的游戏数据包
    • 端口映射:把公网的某个端口转发到内网某台机器的指定端口

    为什么选LVS?

    数据库和游戏服务器使用的是TCP协议,不是HTTP协议,它们的数据格式是自定义的二进制协议,没有HTTP头这种概念。

    text

    MySQL协议数据包:
    [包头][包体]  ← 全是二进制,没有"域名"或"URL"这个字段

    所以用Nginx(七层)来处理MySQL流量是无效的,因为Nginx只能解析HTTP协议,看不懂MySQL协议。

    LVS(四层) 的做法是:

    只改IP和端口,不碰数据包内容。

    text

    客户端(128) → 访问 192.168.226.10:3306 (VIP)
    LVS(四层) → 把目标IP改成 192.168.226.20:3306 (后端MySQL)
    数据包内容 → 原封不动,MySQL协议数据完全保留

    为什么LVS效率高?

    对比项 LVS(四层) Nginx(七层)
    拆包 不拆,只看IP+端口 拆,读HTTP头
    CPU消耗 极低 较高
    延迟 微秒级 毫秒级
    吞吐量 非常高 中等

    选型结论

    只要你的流量不是HTTP协议(如MySQL、Redis、游戏),就用四层负载均衡(LVS/HAProxy的tcp模式)。

    三、实际生产架构:四层 + 七层混合使用

    真正的大规模系统,通常四层和七层配合使用,各司其职:

    text

    互联网流量
        ↓
    【四层负载均衡 LVS】(扛住千万级并发)
        ↓ 只做IP+端口转发,不拆包,效率极高
        ↓
    【七层负载均衡 Nginx 集群】(多台)
        ↓ 拆包,读域名/URL/Cookie
        ↓
    【后端应用服务器】(Tomcat、PHP、Node.js)

    分工:

    • LVS(四层):站在最前面,负责扛住大流量,把请求分发给后端的多台Nginx。
    • Nginx(七层):负责做智能路由(按域名/URL分发),处理会话保持,传递真实IP。
    • 后端服务器:真正执行业务逻辑。

    四、一句话总结

    场景 用什么 核心原因
    Web服务(HTTP) Nginx(七层) 需要读域名、URL、Cookie来做智能转发
    数据库/端口转发(TCP) LVS(四层) 看不懂HTTP协议,只需要快速转包,效率最高
    大规模生产 LVS+ Nginx混合 四层扛流量,七层做路由,各取所长

    记住:七层能干四层的活(但没必要,效率低),四层干不了七层的活(因为看不懂HTTP)。这就是为什么Web服务必须用七层,而数据库转发用四层就够了。


    Nginx upstream虚拟配置语法

    Syntax: upstream name { ... }
    Default: -
    Context: http
    
    #upstream例
    upstream backend {
        server backend1.example.com       weight=5;
        server backend2.example.com:8080;
        server unix:/tmp/backend3;
        server backup1.example.com:8080   backup;
    }
    server {
        location / {
            proxy_pass http://backend;
        }
    }

    Web01服务器上配置Nginx

    [root@web01 ~]# cd /etc/nginx/conf.d/
    [root@web01 conf.d]# vim node.conf
    server {
        listen 80;
        server_name node.test.com;
        location / {
            root /code/node;
            index index.html;
        }
    }
    [root@web01 conf.d]# mkdir -p /code/node
    [root@web01 conf.d]# echo "web01 ..." > /code/node/index.html
    [root@web01 conf.d]# systemctl restart nginx
    # 关闭防火墙和SElinux
    [root@web02 conf.d]# setenforce 0
    [root@web02 conf.d]# systemctl stop firewalld

    Web02服务器上配置Nginx

    [root@web02 ~]# cd /etc/nginx/conf.d/
    [root@web02 conf.d]# vim node.conf
    server {
        listen 80;
        server_name node.test.com;
        location / {
            root /code/node;
            index index.html;
        }
    }
    [root@web02 conf.d]# mkdir -p /code/node
    [root@web02 conf.d]# echo "web02 ..." > /code/node/index.html
    [root@web02 conf.d]# systemctl restart nginx
    # 关闭防火墙和SElinux
    [root@web02 conf.d]# setenforce 0
    [root@web02 conf.d]# systemctl stop firewalld

    配置Nginx负载均衡

    [root@lb01 ~]# cd /etc/nginx/conf.d/
    
    [root@lb01 conf.d]# vim /etc/nginx/proxy_params
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    
    proxy_connect_timeout 30;
    proxy_send_timeout 60;
    proxy_read_timeout 60;
    
    proxy_buffering on;
    proxy_buffer_size 32k;
    proxy_buffers 4 128k;
    
    [root@lb01 conf.d]# vim node_proxy.conf
    upstream node {
        server 192.168.226.20:80;
        server 192.168.226.30:80;
    }
    server {
        listen 80;
        server_name node.test.com;
    
        location / {
            proxy_pass http://node;
            include proxy_params;
        }
    }
    
    [root@lb01 conf.d]# nginx -t
    [root@lb01 conf.d]# systemctl restart nginx

    因为 upstream node { ... } 这个块定义了 node 这个名字,所以当你在 proxy_pass http://node; 里写 node 时,Nginx 就知道你要找的是这个 upstream 块里定义的服务器列表。


    详细解释:node 是怎么变成后端地址的?

    第一步:你定义了 upstream 块

    nginx

    upstream node {                    # ← 这里定义了一个"上游服务器组",名字叫 node
        server 192.168.226.20:80;      # 后端服务器1
        server 192.168.226.30:80;      # 后端服务器2
    }

    upstream 是 Nginx 的一个指令,作用是定义一个后端服务器组,并给它起个名字(这里叫 node)。

    第二步:proxy_pass 引用这个名字

    nginx

    location / {
        proxy_pass http://node;        # ← 这里的 node 就是上面定义的 upstream 名字
    }

    当Nginx 看到 proxy_pass http://node; 时,它会:

    1. 去查找有没有定义过名叫 node 的 upstream 块
    2. 找到了,就按照 upstream node 里定义的服务器列表去转发
    3. 按照负载均衡算法(默认轮询),选中一台后端服务器
    4. 把请求转发给 192.168.226.20:80 或 192.168.226.30:80

    类比理解

    概念 类比
    upstream node { ... } 你手机通讯录里存了一个联系人,名字叫”老王”,里面存了老王的手机号
    proxy_pass http://node; 你发微信说”转发给老王”
    实际转发到 192.168.226.20:80 微信后台查到”老王”的手机号,把消息发过去了

    所以 node 只是一个别名,Nginx会根据这个别名去 upstream 块里找到真正的 IP 地址。


    如果你不写 upstream 会怎样?

    你也可以直接在 proxy_pass 里写 IP:

    nginx

    location / {
        proxy_pass http://192.168.226.20:80;   # 不经过 upstream,直接写死 IP
    }

    这样就不用定义 upstream node 了。但这样写的问题是:

    • 只能转发给一台后端服务器,做不了负载均衡
    • 如果后端服务器IP变了,需要手动改配置

    所以生产环境都推荐用 upstream 方式,方便扩展和管理。


    总结

    node 不是自动生效的,是因为你在 upstream node 里定义了它,Nginx 才知道 node 代表什么。这是一种”先定义、后引用”的配置逻辑。

    配置 作用
    upstream node { server 20:80; server 30:80; } 定义:把 20 和 30 这两台机器打包,起名叫 node
    proxy_pass http://node; 引用:使用上面定义的 node 组进行转发

    实践

    1. 代理服务器

    image-20260827163334072

    Nginx 的 include 指令支持相对路径,相对的是 Nginx 的主配置文件所在目录,也就是 /etc/nginx/。

    路径解析规则

    写法 Nginx 实际查找路径
    include proxy_params; /etc/nginx/proxy_params
    include conf.d/node_proxy.conf; /etc/nginx/conf.d/node_proxy.conf
    include /etc/nginx/proxy_params; /etc/nginx/proxy_params(绝对路径)

    所以你在 /etc/nginx/conf.d/ 目录下写 include proxy_params;,Nginx并不会去 conf.d/ 里找,而是回到 /etc/nginx/ 目录去找这个文件。

    image-20260827164253896

    1. 后端web服务器

    image-20260827171051461

    1. 客户端,域名轮询访问

    image-20260827171923649


    Nginx负载均衡调度算法

    调度算法 概述
    轮询 按时间顺序逐一分配到不同的后端服务器(默认)
    weight 加权轮询,weight值越大,分配到的访问几率越高
    ip_hash 每个请求按访问IP的hash结果分配,这样来自同一IP的固定访问一个后端服务器
    url_hash 按照访问URL的hash结果来分配请求,是每个URL定向到同一个后端服务器
    least_conn 最少链接数,那个机器链接数少就分发

    1. RR – 轮询(Round-Robin)

    默认算法,请求按顺序轮流分发到后端服务器。

    nginx

    upstream load_pass {
        server 192.168.226.10:80;
        server 192.168.226.20:80;
    }

    特点:

    • 每个请求按时间顺序逐一分配到不同后端服务器
    • 适合后端服务器性能一致的场景
    • 无需额外配置

    2. WR – 权重轮询(Weighted Round-Robin)

    通过 weight 参数分配权重,权重越高分配到的请求越多。

    nginx

    upstream load_pass {
        server 192.168.226.10:80 weight=5;   # 权重5,处理5份请求
        server 192.168.226.20:80;            # 默认权重1,处理1份请求
    }

    特点:

    • 权重比例 = 5:1,每6次请求中5次去web01,1次去web02
    • 适合服务器性能不同的场景
    • 高性能服务器设置更高的权重

    3. ip_hash – IP哈希

    根据客户端IP地址进行哈希计算,确保同一客户端IP始终访问同一台后端服务器。

    nginx

    upstream load_pass {
        ip_hash;
        server 192.168.226.10:80;
        server 192.168.226.20:80;
    }

    特点:

    • 保持会话(Session)一致性
    • 适合有状态的应用(如购物车、登录状态)
    • 不能与 weight 一起使用
    • 如果所有客户端经过同一个代理访问,ip_hash 看到的是代理IP,会导致所有请求转发到同一台后端服务器

    注意事项:

    nginx

    # ❌ 错误:ip_hash 不能和 weight 一起使用
    upstream load_pass {
        ip_hash;
        server 192.168.226.10:80 weight=5;   # 会报错!
        server 192.168.226.20:80;
    }
    
    # ✅ 正确:ip_hash 单独使用
    upstream load_pass {
        ip_hash;
        server 192.168.226.10:80;
        server 192.168.226.20:80;
    }

    4. least_conn – 最少连接

    将请求分发给当前活动连接数最少的服务器。

    nginx

    upstream load_pass {
        least_conn;
        server 192.168.226.10:80;
        server 192.168.226.20:80;
    }

    特点:

    • 适合长连接场景(如WebSocket、数据库连接)
    • 避免请求堆积在某台服务器上

    5. fair – 响应时间优先(第三方模块)

    根据后端服务器的响应时间来分配请求,响应时间短的优先。

    nginx

    upstream load_pass {
        fair;
        server 192.168.226.10:80;
        server 192.168.226.20:80;
    }

    特点:

    • 需要安装第三方模块
    • 动态调整,响应快的服务器处理更多请求

    二、服务器状态参数

    nginx

    upstream load_pass {
        server 192.168.226.10:80 weight=5 max_fails=3 fail_timeout=30s;
        server 192.168.226.20:80 weight=2;
        server 192.168.226.30:80 backup;
        server 192.168.226.40:80 down;
    }

    1. weight(权重)

    nginx

    server 192.168.226.10:80 weight=5;

    说明:

    • 默认值为1
    • 权重值越高,分配到的请求越多
    • 适合后端服务器硬件配置不同的场景

    2. max_fails(最大失败次数)

    nginx

    server 192.168.226.10:80 max_fails=3;

    正确理解:

    • 在 fail_timeout 指定的时间窗口内,如果失败次数达到 max_fails 次,nginx 就认为该服务器不可用
    • 失败包括:连接超时、读取超时、后端返回 5xx 错误等
    • 默认值为 1,即一次失败就标记为不可用

    3. fail_timeout(失败超时时间)

    nginx

    server 192.168.226.10:80 max_fails=3 fail_timeout=30s;

    正确理解:

    • fail_timeout 有两个作用:
      1. 失败统计的时间窗口:在 30s 内如果失败3次,标记该服务器为不可用
      2. 标记不可用的持续时间:被标记为不可用后,nginx 会在 30s 后再次尝试连接该服务器

    工作流程:

    text

    时间轴:
    0s -------- 10s -------- 20s -------- 30s -------- 40s -------- 50s
    |           |            |            |            |            |
    请求1失败   请求2失败    请求3失败    标记不可用   尝试恢复    恢复成功
                                        (30s内3次失败) 
                                        (30s后自动尝试)

    4. backup(备份服务器)

    nginx

    server 192.168.226.30:80 backup;

    正确理解:

    • 只有当所有主服务器都不可用时,才会将请求转发给 backup 服务器
    • backup 服务器平时不处理请求,只作为备用
    • 用于高可用保障

    5. down(下线标记)

    nginx

    server 192.168.226.40:80 down;

    正确理解:

    • 永久标记该服务器为不可用
    • nginx 不会向该服务器转发任何请求
    • 常用于服务器维护或升级时,优雅地移除服务器
    • 需要恢复时,手动注释掉 down 或删除该行

    三、参数对照表

    参数 作用 默认值 注意事项
    weight 权重,值越大分配请求越多 1
    max_fails fail_timeout 时间内允许的最大失败次数 1 达到后标记为不可用
    fail_timeout ① 失败统计时间窗口 ② 标记不可用的持续时间 10s 两个作用合并在一个参数中
    backup 备份服务器,主服务器全挂时启用 无
    down 永久标记为不可用 无 用于维护场景
    max_conns 最大并发连接数 0(无限制) 需要商业版
    resolve 动态解析域名 无 后端使用域名时配置

    四、配置场景选择

    场景 推荐算法 原因
    无状态应用(API、静态资源) RR 轮询 简单、均衡、无需额外配置
    服务器性能不同 WR 权重轮询 高性能服务器处理更多请求
    需要会话保持(登录状态、购物车) ip_hash 同一用户始终访问同一后端
    长连接、WebSocket 场景 least_conn 避免连接集中在某台服务器
    响应时间敏感场景 fair 响应快的服务器优先处理

    五、常见问题

    Q1:ip_hash 计算的是代理服务器的IP吗?

    A:默认计算 $remote_addr(客户端真实IP)。但如果客户端经过代理访问,nginx看到的是代理服务器的IP,会导致所有请求都转发到同一台后端服务器。

    解决方案:

    nginx

    # 获取真实客户端IP
    set_real_ip_from 代理服务器IP段;
    real_ip_header X-Forwarded-For;
    
    upstream load_pass {
        ip_hash;
        server 192.168.226.10:80;
        server 192.168.226.20:80;
    }

    Q2:max_fails 和 fail_timeout 如何配合工作?

    A:在 fail_timeout 时间内,如果失败次数达到 max_fails 次,nginx 标记该服务器为不可用。经过 fail_timeout 时间后,nginx 会再次尝试连接。

    text

    示例配置:max_fails=3 fail_timeout=30s
    
    1. 30s 内失败3次 → 标记不可用
    2. 30s 后自动尝试恢复连接
    3. 恢复成功 → 重新加入负载均衡池
    4. 恢复失败 → 继续标记为不可用,等待下一个30s

    Q3:backup 和 down 的区别?

    A:

    参数 状态 何时接收请求 使用场景
    backup 动态 主服务器全部不可用时 高可用保障
    down 永久 永不接收请求 服务器维护

    六、完整配置示例

    nginx

    upstream node {
        # 主服务器:权重5,失败3次标记不可用,30s后重试
        server 192.168.226.10:80 weight=5 max_fails=3 fail_timeout=30s;
    
        # 主服务器:默认权重1,失败3次标记不可用,60s后重试
        server 192.168.226.20:80 max_fails=3 fail_timeout=60s;
    
        # 备份服务器:主服务器全挂时启用
        server 192.168.226.30:80 backup;
    
        # 下线服务器:正在维护中
        server 192.168.226.40:80 down;
    }
    
    server {
        listen 80;
        server_name node.test.com;
    
        location / {
            proxy_pass http://node;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
    
            proxy_connect_timeout 60s;
            proxy_send_timeout 60s;
            proxy_read_timeout 60s;
        }
    }

    upstream 配置位置

    text

    客户端 (192.168.226.40)
        ↓ 访问 http://node.test.com
        ↓
    【代理服务器/负载均衡器】 (192.168.226.10)  ← 在这里配置 upstream!
        ↓ /etc/nginx/conf.d/node.conf
        ↓ upstream node { ... }
        ├──→ 后端1 (192.168.226.20:80)  ← 被代理的服务器
        └──→ 后端2 (192.168.226.30:80)  ← 被代理的服务器

    Nginx 负载均衡会话保持详解

    一、什么是会话保持(Session Stickiness)?

    在负载均衡环境下,用户的请求会被分发到不同的后端服务器。

    text

    用户A → 负载均衡器 → 第1次请求 → web01(登录成功,session保存在web01)
    用户A → 负载均衡器 → 第2次请求 → web02(web02没有用户的session,需要重新登录!)❌

    会话保持的目的:确保同一个用户的请求始终被转发到同一台后端服务器,或者多台服务器共享session数据。

    二、Session 和 Cookie 的区别

    1. Cookie(客户端存储)

    text

    浏览器(客户端)
        ↓ 存储位置:浏览器本地
        ↓ 存储内容:用户标识信息(如 session_id)
        ↓ 特点:
            - 大小限制:一般 4KB 左右
            - 自动发送:每次请求会自动携带
            - 安全性:数据可见,可被篡改(需加密)

    Cookie 是钥匙,Session 是保险柜。

    2. Session(服务端存储)

    text

    服务器
        ↓ 存储位置:服务器内存/文件/数据库
        ↓ 存储内容:用户实际数据(用户名、权限、购物车等)
        ↓ 特点:
            - 安全:数据存储在服务端,用户无法篡改
            - 灵活:可以存储任意类型的数据
            - 生命周期:由服务器控制

    3. 两者关系

    text

    用户登录流程:
    
    1. 用户输入账号密码 → 提交到服务器
    2. 服务器验证通过 → 创建 Session(存储用户信息)
    3. 服务器生成 Session ID → 通过 Cookie 返回给浏览器
    4. 浏览器保存 Cookie(包含 Session ID)
    5. 后续请求 → 浏览器自动携带 Cookie
    6. 服务器根据 Session ID → 找到对应的 Session 数据

    text

    ┌─────────────┐      ┌─────────────┐      ┌─────────────┐
    │   浏览器     │      │   负载均衡器 │      │   web01     │
    │             │      │             │      │             │
    │  Cookie     │ ──→  │  分发请求   │ ──→  │  Session    │
    │  session_id │      │             │      │  用户数据    │
    │  = abc123   │      │             │      │             │
    └─────────────┘      └─────────────┘      └─────────────┘
                                                     ↓
                                                同一个用户
                                                session_id=abc123

    image-20260828184320082

    image-20260828184712765

    实践会话保持

    纯逻辑架构图(服务依赖关系)

    text

                            【用户请求】
                                |
                                v
                      ┌─────────────────┐
                      │    Nginx        │  ← 静态资源(图片/CSS/JS)直接返回
                      │  (HTTP入口)     │  ← 动态请求转发给 PHP-FPM
                      └────────┬────────┘
                               │ (FastCGI 协议)
                               v
                      ┌─────────────────┐
                      │   PHP-FPM       │  ← 执行网站业务代码
                      │  (应用运行时)   │  ← phpMyAdmin 也运行在这里(管理工具)
                      └────────┬────────┘
                               │
              ┌────────────────┼────────────────┐
              │                │                │
              v                v                v
       ┌───────────┐   ┌───────────┐   ┌───────────┐
       │   Session │   │  业务缓存  │   │  持久化   │
       │   (会话)  │   │  (查询缓存)│   │  数据存储 │
       └─────┬─────┘   └─────┬─────┘   └─────┬─────┘
             │               │               │
             └───────────────┼───────────────┘
                             │
                             v
                      ┌─────────────────┐
                      │     Redis       │  ← 缓存层(临时存储)
                      │  (内存数据库)   │  ← 存储:登录态、验证码、热点数据
                      └─────────────────┘
                             │
                             │(缓存未命中时回源)
                             v
                      ┌─────────────────┐
                      │    MySQL        │  ← 持久化层(永久存储)
                      │  (关系型数据库) │  ← 存储:用户表、订单表、商品表
                      └─────────────────┘

    准备工作安装Lnmp架构环境

    在LNMP架构中为Nginx服务器(服务器A)安装PHP运行环境和扩展的完整命令

    dnf install -y epel-release
    dnf groupinstall "Development Tools" -y
    dnf install -y wget gcc make libxml2-devel openssl-devel sqlite-devel libcurl-devel libpng-devel libjpeg-devel oniguruma libzip-devel
    dnf install -y php php-cli php-fpm php-mysqlnd php-zip php-gd php-mbstring php-curl php-xml php-pear php-bcmath php-intl
    dnf install -y php-pecl-redis
    php -m | grep redis

    架构图

    text

    ┌─────────────────────────────────────────────────────────────────────────┐
    │                          【内网环境 192.168.226.0/24】                  │
    │                                                                         │
    │  ┌─────────────────────┐    ┌──────────────────────────────────────┐   │
    │  │   【数据库服务器】    │    │          【Web服务器集群】           │   │
    │  │   192.168.226.10    │    │                                      │   │
    │  │                     │    │   ┌────────────────────────────┐    │   │
    │  │   ┌─────────────┐   │    │   │   Web服务器1              │    │   │
    │  │   │  MariaDB    │   │    │   │   192.168.226.20          │    │   │
    │  │   │  端口:3306  │◄──┼────┼───│   Nginx + PHP-FPM        │    │   │
    │  │   └─────────────┘   │    │   │   phpMyAdmin             │    │   │
    │  │                     │    │   └────────────────────────────┘    │   │
    │  │   远程连接账号:      │    │                                      │   │
    │  │   root@'192.168.    │    │   ┌────────────────────────────┐    │   │
    │  │   226.%'            │    │   │   Web服务器2              │    │   │
    │  │   密码:123456       │    │   │   192.168.226.30          │    │   │
    │  └─────────────────────┘    │   │   Nginx + PHP-FPM        │    │   │
    │                              │   │   phpMyAdmin             │    │   │
    │                              │   └────────────────────────────┘    │   │
    │                              └──────────────────────────────────────┘   │
    │                                                                         │
    │  数据流向:Web服务器(20/30) ---> 远程连接 ---> 数据库服务器(10):3306   │
    │  访问入口:用户 ---> Web服务器(20或30) ---> phpMyAdmin管理数据库        │
    └─────────────────────────────────────────────────────────────────────────┘
    • 在10服务器上部署mariadb数据库,提供phpmyadmin访问
    # 安装mariadb数据库软件
    yum install mariadb-server mariadb -y
    
    # 启动数据库并且设置开机自启动
    systemctl start mariadb
    systemctl enable mariadb
    
    # 设置mariadb的密码
    mysqladmin password '123456'
    
    # 验证数据库是否工作正常
    mysql -uroot -p123456 -e "show databases;"
    +--------------------+
    | Database           |
    +--------------------+
    | information_schema |
    | mysql              |
    | performance_schema |
    | test               |
    +--------------------+
    
    # 创建访问用户
    grant all privileges on *.* to root@'192.168.226.%' identified by '123456' with grant option;
    flush privileges;
    部署LNMP
    [root@server2 ~]# yum install nginx -y
    [root@server2 ~]# vim /etc/php-fpm.d/www.conf 
    listen = 9000
    [root@server2 ~]# sed -i '/^user/c user = nginx' /etc/php-fpm.d/www.conf
    [root@server2 ~]# sed -i '/^group/c group = nginx' /etc/php-fpm.d/www.conf
    [root@server2 ~]# systemctl restart php-fpm.service 
    [root@server2 ~]# ss -tnl
    [root@server2 ~]# cd /etc/nginx/conf.d/
    [root@server2 ~]# vim php.conf 
    server {
        listen 80;
        server_name php.test.com;
        root /code/phpmyadmin;
    
        location / {
            index index.php index.html;
        }
    
        location ~ .php$ {
            fastcgi_pass 127.0.0.1:9000;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            include fastcgi_params;
        }
    }
    [root@server2 ~]# nginx -t
    [root@server2 ~]# nginx -s reload
    [root@server2 ~]# mkdir /code
    [root@server2 ~]# cd /code/
    [root@server2 ~]# unzip phpMyAdmin-5.1.1-all-languages.zip 
    [root@server2 ~]# mv phpMyAdmin-5.1.1-all-languages phpmyadmin
    [root@server2 ~]# chown -R nginx.nginx /var/lib/php/
    [root@server3 ~]# cd /code/phpmyadmin/
    [root@server2 phpmyadmin]# cp config.sample.inc.php config.inc.php
    [root@server2 phpmyadmin]# vim config.inc.php
    $cfg['Servers'][$i]['host'] = '192.168.226.10';
    

    为什么写 listen = 9000?

    因为你的 Nginx 配置里写的是:

    nginx

    fastcgi_pass 127.0.0.1:9000;

    这两边必须一致,否则 PHP-FPM 收不到 Nginx 转发的请求。


    直观理解

    text

    Nginx 把请求发到 → 127.0.0.1:9000
                            ↓
                     PHP-FPM 必须在这个地址等着
                            ↓
                     所以 PHP-FPM 要写 listen = 9000

    如果两边不一致:

    Nginx 转发到 PHP-FPM 监听 结果
    127.0.0.1:9000 listen = 9000 ✅ 能通信
    127.0.0.1:9000 listen = /run/php-fpm/www.sock ❌ 找不到,报错

    因为 phpMyAdmin 需要一个 config.inc.php 才能连接数据库,而官方只提供了一个模板文件 config.sample.inc.php,你需要复制一份再修改。


    详细解释

    文件 作用 能不能直接改
    config.sample.inc.php 配置模板,官方提供的示例 ❌ 不能直接改,改了下次升级会覆盖
    config.inc.php 真正的配置文件,phpMyAdmin 实际读取的 ✅ 复制一份来改

    第1步:编辑 PHP-FPM配置文件

    bash

    [root@server2 ~]# vim /etc/php-fpm.d/www.conf
    部分 含义
    vim Vi IMproved,Linux 下最常用的文本编辑器
    /etc/php-fpm.d/www.conf PHP-FPM 的进程池配置文件路径

    这个文件里配置什么?

    • listen:PHP-FPM 监听的地址和端口
    • user/group:PHP-FPM 进程的运行用户和组
    • pm:进程管理方式(dynamic/static)
    • pm.max_children:最大子进程数

    为什么叫 www.conf? PHP-FPM支持多个进程池(pool),每个池可以有独立的配置。www.conf 是默认的进程池配置文件。如果你有多个网站,可以复制成 blog.conf、shop.conf 等。


    text

            /etc/php-fpm.d/  (配置目录)
                  │
        ┌─────────┼─────────┬──────────┐
        ▼         ▼         ▼          ▼
     ┌──────┐ ┌──────┐ ┌──────┐  ┌────────┐
     │www.conf│ │blog.conf│ │shop.conf│ │api.conf│  ← 所有 .conf 文件
     └──────┘ └──────┘ └──────┘ └────────┘
        │         │         │          │
        ▼         ▼         ▼          ▼
      PHP-FPM 自动加载并管理所有这些池

    核心机制

    PHP-FPM 的加载逻辑

    PHP-FPM 主程序启动时,会自动扫描并加载 /etc/php-fpm.d/ 目录下所有以 .conf 结尾的文件。

    这个行为由 PHP-FPM 主配置文件决定:

    bash

    # /etc/php-fpm.conf(主配置文件)
    include=/etc/php-fpm.d/*.conf   # ← 这行告诉 PHP-FPM:加载这个目录下所有 .conf

    每个 .conf 文件 = 一个独立的进程池

    文件 池名称 作用
    www.conf [www] 处理网站A的PHP请求
    blog.conf [blog] 处理网站B的PHP请求
    shop.conf [shop] 处理网站C的PHP请求
    api.conf [api] 处理API服务的PHP请求

    实际网络流程

    当用户请求一个 PHP 文件时:

    text

    用户访问 http://192.168.226.20/index.php
                  │
                  ▼
    ┌──────────────────────────────────────────────────────────────┐
    │                     server2 (192.168.226.20)                 │
    │                                                             │
    │  ① Nginx 收到请求(监听 80 端口)                          │
    │     │                                                       │
    │  ② 匹配到 location ~ .php$                                │
    │     │                                                       │
    │  ③ fastcgi_pass 127.0.0.1:9000                            │
    │     │                                                       │
    │     └─────▶ 数据包走"回环接口(loopback)"                 │
    │             不经过网卡,不经过交换机,直接在内存里转发      │
    │             目的地:127.0.0.1:9000(本机)                  │
    │     │                                                       │
    │  ④ PHP-FPM 正在 9000 端口等待,接收到请求                  │
    │     │                                                       │
    │  ⑤ PHP-FPM 执行 /code/phpmyadmin/index.php                 │
    │     │                                                       │
    │  ⑥ index.php 连接 192.168.226.10:3306(数据库服务器)       │
    │     │                                                       │
    │  ⑦ 数据返回 → Nginx → 用户浏览器                          │

    text

    用户访问 http://php.test.com/index.php
                  │
                  ▼
        ┌─────────────────────────────────┐
        │         Nginx (本机)            │
        │                                  │
        │  ① 请求的是 .php 文件           │
        │     ↓                            │
        │  ② 匹配 location ~ .php$       │
        │     ↓                            │
        │  ③ fastcgi_pass 127.0.0.1:9000 │
        │     (转发给本机 9000 端口)     │
        └─────────────────────────────────┘
                  │
                  ▼
        ┌─────────────────────────────────┐
        │       PHP-FPM (本机)            │
        │                                  │
        │  ④ 监听 9000 端口               │
        │     ↓                            │
        │  ⑤ 找到 www.conf 池             │
        │     ↓                            │
        │  ⑥ 执行 index.php               │
        │     ↓                            │
        │  ⑦ 连接 192.168.226.10:3306     │
        │     (数据库服务器)             │
        └─────────────────────────────────┘
                  │
                  ▼
        ┌─────────────────────────────────┐
        │   数据库服务器 192.168.226.10    │
        │   返回数据给 PHP-FPM            │
        └─────────────────────────────────┘
                  │
                  ▼
        Nginx → 用户浏览器显示页面

    text

    用户请求 http://php.test.com/index.php(.php 结尾)
                  │
                  ▼
    ┌─────────────────────────────────────────────────────────────┐
    │  ① Nginx 收到请求                                          │
    │     看到是 .php 结尾 → 匹配 location ~ .php$              │
    │                                                             │
    │  ② location 里配置了 fastcgi_pass 127.0.0.1:9000         │
    │     意思:把这个请求交给本机的 PHP-FPM 管理                 │
    │                                                             │
    │  ③ 安装 Nginx 的时候,同时也装了 php-fpm                   │
    │     (yum install php-fpm)                                 │
    └─────────────────────────────────────────────────────────────┘
                  │
                  ▼
    ┌─────────────────────────────────────────────────────────────┐
    │  ④ PHP-FPM 是一个"进程管理器"                              │
    │     ● Master 进程:负责管理下面所有的 Worker 进程          │
    │     ● Worker 进程:真正执行 PHP 代码的进程                 │
    │                                                             │
    │  ⑤ www.conf 定义了:                                       │
    │     ● listen = 9000(监听 9000 端口)                     │
    │     ● user = nginx(以 nginx 用户身份运行)               │
    │     ● 这个进程池就是专门处理 phpMyAdmin 请求的             │
    │                                                             │
    │  ⑥ Worker 进程执行 /code/phpmyadmin/index.php              │
    │     也就是执行进程池里的这个 PHP 文件                       │
    └─────────────────────────────────────────────────────────────┘
                  │
                  ▼
    ┌─────────────────────────────────────────────────────────────┐
    │  ⑦ index.php 里 require 了 config.inc.php                  │
    │     config.inc.php 里配置了:                               │
    │     $cfg['Servers'][$i]['host'] = '192.168.226.10';        │
    │                                                             │
    │  ⑧ 这个配置的意思是:                                       │
    │     "我要连接远程的数据库服务器(192.168.226.10)"          │
    └─────────────────────────────────────────────────────────────┘
                  │
                  ▼
    ┌─────────────────────────────────────────────────────────────┐
    │  ⑨ PHP 代码执行 SQL 查询(如 SELECT * FROM users)        │
    │     通过网络发送到 192.168.226.10:3306                      │
    │                                                             │
    │  ⑩ MySQL 数据库执行查询,把结果集(数据)返回给 PHP       │
    └─────────────────────────────────────────────────────────────┘
                  │
                  ▼
            PHP 拼成 HTML 页面
                  │
                  ▼
            Nginx 返回给用户浏览器

    image-20260828133601487

    image-20260828191510535

    接入负载均衡

    [root@lb01 ~]# vim /etc/nginx/conf.d/proxy_php.com.conf
    upstream php {
            server 192.168.226.20:80;
            server 192.168.226.30:80;
    }
    server {
            listen 80;
            server_name php.test.com;
            location / {
                    proxy_pass http://php;
                    include proxy_params;
            }
    }
    
    [root@lb01 ~]# nginx -t
    [root@lb01 ~]# systemctl restart nginx
    • 常用优化配置
      • Proxy代理网站常用优化配置如下,将配置写入新文件,调用时使用include引用即可
    [root@Nginx ~]# vim /etc/nginx/proxy_params
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    
    proxy_connect_timeout 30;
    proxy_send_timeout 60;
    proxy_read_timeout 60;
    
    proxy_buffering on;
    proxy_buffer_size 32k;
    proxy_buffers 4 128k;

    image-20260828193027052


    准备redis内存数据库

    [root@server1 ~]# yum -y install redis
    [root@server1 ~]# vim /etc/redis/redis.conf 
    bind 0.0.0.0
    [root@server1 ~]# systemctl enable --now redis
    Created symlink /etc/systemd/system/multi-user.target.wants/redis.service → /usr/lib/systemd/system/redis.service.
    [root@localhost ~]# ss -tnl
    State                    Recv-Q                   Send-Q                                     Local Address:Port                                       Peer Address:Port
    LISTEN                   0                        128                                              0.0.0.0:22                                              0.0.0.0:*
    LISTEN                   0                        511                                              0.0.0.0:80                                              0.0.0.0:*
    LISTEN                   0                        511                                              0.0.0.0:6379                                            0.0.0.0:*
    LISTEN                   0                        80                                                     *:3306                                                  *:*
    LISTEN                   0                        128                                                 [::]:22                                                 [::]:*
    LISTEN                   0                        511                                                 [::]:80                                                 [::]:*
    

    bind 参数详解:

    配置 含义 谁能连接
    bind 127.0.0.1 只监听本机(默认) 只有本机可以连接
    bind 0.0.0.0 监听所有 IP 地址 任何机器都可以连接(需密码保护)
    bind 192.168.1.100 只监听指定 IP 只有该 IP 网段的机器可连

    为什么要改成 0.0.0.0? 因为你的架构中,Web 服务器(192.168.226.20)需要连接Redis服务器(192.168.226.10),如果不改,Redis只监听本机,其他机器连不上。

    安全提醒:bind 0.0.0.0 允许任何人连接,生产环境建议:

    1. 设置密码(requirepass)
    2. 用防火墙限制只允许特定IP访问

    [root@server2 ~]# vim /etc/php.ini
    [Session]
    ;session.save_handler = files
    session.save_handler = redis
    session.save_path = "tcp://192.168.226.10:6379"
    ;session.save_path = "tcp://192.168.226.10:6379?auth=centos" #如果redis存在密码,则使用该方式
    session.auto_start = 1
    
    [root@server2 ~]# vim /etc/php-fpm.d/www.conf
    #注释php-fpm.d/www.conf里面的两条内容,否则session内容会一直写入/var/lib/php/session目录中
    ;php_value[session.save_handler] = files
    ;php_value[session.save_path]    = /var/lib/php/session
    [root@server2 conf.d]# systemctl restart php-fpm.service

    1. 核心目标:将Session存储从“文件”切换到“Redis”

    默认情况下,PHP的Session数据存储在服务器硬盘的文件中(/var/lib/php/session)。你的配置旨在将所有Session数据迁移到Redis内存数据库中,以提升高并发下的读写速度,并方便多台Web服务器共享Session。


    2. 逐步配置详解

    第一步:修改 /etc/php.ini(全局生效)

    这是PHP的核心配置文件,你在这里做了三处关键修改:

    • session.save_handler = redis 告诉PHP,不再使用默认的files(文件)方式,而是使用redis扩展模块来处理Session。
    • session.save_path = "tcp://192.168.226.10:6379" 指定Redis服务器的IP和端口。tcp://是协议头,告诉PHP使用TCP连接。
    • session.auto_start = 1 这个设置意味着每次请求开始时,PHP会自动调用session_start()。 ⚠️ 注意:启用后,如果你的某些页面不需要Session,也会白白占用Redis连接资源。通常建议在代码中手动调用session_start(),这里设为1是为了简化测试或老旧项目的改造。

    第二步:修改 /etc/php-fpm.d/www.conf(覆盖全局)

    PHP-FPM(处理动态请求的进程管理器)有自己的配置,可以覆盖php.ini的设置。

    • 你注释掉的两行:

      ini

      ;php_value[session.save_handler] = files
      ;php_value[session.save_path]    = /var/lib/php/session

      这两行的原始作用是强制将Session存回文件。如果不注释,它们会覆盖你在php.ini中设置的redis,导致Session依然写入硬盘。注释掉它们,就等于移除了这个“覆盖指令”,让php.ini中的Redis配置正式生效。


    3. 为什么需要两步配置?(关键点)

    • php.ini 是“最高指示”,但PHP-FPM的www.conf拥有更高优先级的局部设置权限。
    • 你的操作逻辑是:在php.ini中设定目标为Redis,然后在www.conf中废除干扰项(Files),确保规则统一。

    ==总结==:通过在php.ini中开启Redis存储,并屏蔽www.conf中的文件存储覆盖指令,最终迫使所有PHP Session数据落地到指定的Redis数据库中。

    image-20260829010649788

    image-20260829010919087

    image-20260829011745483

    image-20260829012414648


    Nginx四层负载均衡场景实践

    Nginx如何配置四层负载均衡

    1、通过访问负载均衡的5555端口,实际是后端的web01的22端口在提供服务;

    2、通过访问负载均衡的6666端口,实际是后端的MySQL的3306端口在提供服务。

    • 创建存放四层负载均衡配置文件的目录
    [root@lb01 ~]# yum install nginx-mod-stream
    [root@lb01 ~]# vim /etc/nginx/nginx.conf
    include /etc/nginx/conf.c/*.conf;
    #include /etc/nginx/conf.d/*.conf;
    [root@lb01 ~]# mkdir /etc/nginx/conf.c
    • 配置四层负载均衡
    [root@lb01 ~]# vim /etc/nginx/conf.c/lb_domain.conf
    stream {
        upstream lb {
                server 192.168.226.20:80 weight=5 max_fails=3 fail_timeout=30s;
                server 192.168.226.30:80 weight=5 max_fails=3 fail_timeout=30s;
        }
    
        server {
                listen 80;
                proxy_connect_timeout 3s;
                proxy_timeout 3s;
                proxy_pass lb;
        }
    }

    二、四层配置代码逐行详解

    nginx

    stream {                                    # ← 四层负载均衡模块
        upstream 1b {                           # ← 定义后端服务器组,名称叫"1b"
            server 192.168.226.20:80 weight=5 max_fails=3 fail_timeout=30s;
            server 192.168.226.30:80 weight=5 max_fails=3 fail_timeout=30s;
        }
    
        server {                                # ← 定义代理服务器
            listen 80;                          # ← 监听本机的80端口
            proxy_connect_timeout 3s;           # ← 连接后端超时时间
            proxy_timeout 3s;                   # ← 数据传输超时时间
            proxy_pass 1b;                      # ← 转发到 upstream 定义的 "1b" 组
        }
    }

    访问流程

    text

    用户浏览器访问 http://192.168.226.10(默认80端口)
                        ↓
            [负载均衡器 lb01] 监听 :80
                        ↓
        四层转发(TCP层,不解析HTTP内容)
                        ↓
        选择后端服务器(根据 weight 权重)
                        ↓
        web01 (192.168.226.20:80)  或  web02 (192.168.226.30:80)
                        ↓
            返回网站首页给用户

    image-20260829021756594image-20260829022129481

    image-20260829022342610


    配置四层负载均衡日志

    • 四层负载均衡开启日志
      • 四层负载均衡是没有access的日志的,因为在nginx.conf的配置中,access的日志格式是配置在HTTP下的,而四层负载均衡配置实在HTTP以外的
      • 如果需要日志则需要配置在stream下面
    [root@lb01 ~]# cd /etc/nginx/conf.c/
    [root@lb01 conf.c]# vim lb_domain.conf
    stream {
        log_format  proxy '$remote_addr $remote_port - [$time_local] $status $protocol '
                      '"$upstream_addr" "$upstream_bytes_sent" "$upstream_connect_time"' ;
        access_log /var/log/nginx/proxy.log proxy;
        upstream lb {
                server 192.168.226.20:80 weight=5 max_fails=3 fail_timeout=30s;
                server 192.168.226.30:80 weight=5 max_fails=3 fail_timeout=30s;
        }
    
        server {
                listen 80;
                proxy_connect_timeout 3s;
                proxy_timeout 3s;
                proxy_pass lb;
        }
    }
    
    [root@lb01 conf.c]# nginx -t
    [root@lb01 conf.c]# systemctl restart nginx

    image-20260829024319062


    Nginx四层负载均衡端口转发

    • 使用Nginx四层负载均衡实现TCP的转发
    请求负载均衡 5555    --->     192.168.226.20:22;
    请求负载均衡 6666    --->     192.168.226.30:3306;
    • 配置Nginx四层负载均衡实现TCP的转发
    [root@lb01 ~]# vim  /etc/nginx/conf.c/lb_domain.conf
    stream {
        log_format  proxy '$remote_addr $remote_port - [$time_local] $status $protocol '
                          '"$upstream_addr" "$upstream_bytes_sent" "$upstream_connect_time"' ;
        access_log /var/log/nginx/proxy.log proxy;
    
    #定义转发ssh的22端口
        upstream ssh {
                server 192.168.226.20:22;
        }
    #定义转发mysql的3306端口
        upstream mysql {
                server 192.168.226.30:3306;
        }
        server {
                listen 5555;
                proxy_connect_timeout 3s;
                proxy_timeout 300s;
                proxy_pass ssh;
        }
    
        server {
                listen 6666;
                proxy_connect_timeout 3s;
                proxy_timeout 3s;
                proxy_pass mysql;
        }
    }
    
    [root@lb01 ~]# nginx -t
    [root@lb01 ~]# setenforce 0
    [root@lb01 ~]# systemctl restart nginx

    image-20260829122429159

    image-20260829122756434


    # 安装mariadb数据库软件
    yum install mariadb-server mariadb -y
    
    # 启动数据库并且设置开机自启动
    systemctl start mariadb
    systemctl enable mariadb
    
    # 设置mariadb的密码
    mysqladmin password '123456'
    
    # 验证数据库是否工作正常
    mysql -uroot -p123456 -e "show databases;"
    
    # 创建访问用户
    grant all privileges on *.* to root@'192.168.226.%' identified by '123456' with grant option;
    flush privileges;
    
    # 测试访问
    [root@server4 ~]# mysql -uroot -p123456 -h 192.168.226.10 -P 6666
    MariaDB [(none)]> 
    [root@server4 ~]# ssh root@192.168.226.10 -p 5555
    root@192.168.226.10 password:

    image-20260829124832276


    Keepalived高可用

    一般是指2台机器启动着完全相同的业务系统,当有一台机器down机了,另外一台服务器就能快速的接管,对于访问的用户是无感知的。
    
    硬件通常使用 F5
    
    软件通常使用 keepalived
    
    keepalived软件是基于VRRP协议实现的,VRRP虚拟路由冗余协议,主要用于解决单点故障问题

    一、为什么需要高可用?

    单点故障问题

    text

    用户 → [Nginx负载均衡器] → 后端服务器
                  ↑
            如果这台机器挂了?
            所有服务全部不可用!

    高可用解决方案

    text

    用户 → [VIP虚拟IP] → 主Nginx (Active)  ← 正常工作
                     ↘  备Nginx (Standby)   ← 随时待命
    
    主Nginx挂了 → VIP自动漂移到备Nginx → 服务不中断!

    二、硬件负载均衡 vs 软件负载均衡

    类型 代表产品 优点 缺点
    硬件 F5、A10、Radware 性能极高、稳定、功能丰富 价格昂贵(几十万起)
    软件 Keepalived、HAProxy、Nginx 免费、灵活、易扩展 性能受限于服务器

    三、VRRP 协议详解(核心!)

    什么是 VRRP?

    VRRP = Virtual Router Redundancy Protocol(虚拟路由冗余协议)

    简单说:把多台路由器虚拟成一台”虚拟路由器”,通过一个虚拟IP对外提供服务。

    四、VRRP 核心概念详解

    1. 虚拟路由器

    text

    ┌─────────────────────────────────────┐
    │         虚拟路由器 (VRRP组)           │
    │  虚拟IP: 192.168.226.100              │
    │  虚拟MAC: 00-00-5e-00-01-51          │
    └─────────────────────────────────────┘
                ↓        ↓
        ┌───────────┐  ┌───────────┐
        │ Master    │  │ Backup    │
        │ 实际IP:   │  │ 实际IP:   │
        │ .10       │  │ .11       │
        └───────────┘  └───────────┘
    • 虚拟IP(VIP):对外提供服务的浮动IP
    • 虚拟MAC:00-00-5e-00-01-{VRID},VRID 范围 1-255

    2. Master(主路由器)

    • 唯一对外提供服务
    • 响应虚拟IP的ARP请求
    • 定期发送 VRRP 通告(心跳包)

    3. Backup(备份路由器)

    • 不对外提供服务
    • 监听 Master 的通告
    • Master 挂了 → 接替成为新的 Master

    五、选举机制(重点!)

    优先级(Priority)

    text

    Master 选举依据:优先级越高越优先
    
    配置示例:
    Master: priority 150
    Backup: priority 100

    抢占模式 vs 非抢占模式

    ① 抢占模式(默认)

    text

    时刻1: Master(150) 运行中 ✅
    时刻2: Master 挂了 ❌
    时刻3: Backup(100) 接管成为新 Master ✅
    时刻4: 原Master恢复(150) → 立即抢回Master位置!(抢占)

    特点:优先级高的永远是 Master

    ② 非抢占模式(nopreempt)

    text

    时刻1: Master(150) 运行中 ✅
    时刻2: Master 挂了 ❌
    时刻3: Backup(100) 接管成为新 Master ✅
    时刻4: 原Master恢复(150) → 继续当Backup,不抢占!

    特点:只要Master不挂,优先级高的也得等着

    六、三种状态机

    text

                        ┌─────────────┐
                        │ Initialize  │ ← 系统启动/Keepalived启动
                        │  (初始化)    │
                        └──────┬──────┘
                               │
                        ┌──────▼──────┐
                        │  检查优先级  │
                        └──────┬──────┘
                               │
                ┌──────────────┼──────────────┐
                ↓              ↓              ↓
        ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
        │   Master    │ │   Backup    │ │   Fault     │
        │   (主状态)   │ │   (备状态)   │ │   (故障)    │
        └─────────────┘ └─────────────┘ └─────────────┘

    ==总结==:都是通过优先级值选举出来的


    Keepalived高可用安装配置

    安装keepalived

    [root@lb01 ~]# yum install -y keepalived
    [root@lb02 ~]# yum install -y keepalived

    配置master

    • 找到配置文件
    [root@lb02 ~]# rpm -qc keepalived
    /etc/keepalived/keepalived.conf
    /etc/sysconfig/keepalived
    • 修改配置
    [root@lb01 ~]# vim /etc/keepalived/keepalived.conf
    global_defs {                   #全局配置
        router_id lb01              #标识身份->名称
    }
    
    vrrp_instance VI_1 {
        state MASTER                #标识角色状态
        interface ens160             #网卡绑定接口
        virtual_router_id 50        #虚拟路由id
        priority 150                #优先级
        advert_int 1                #监测间隔时间
        #use_vmac                  #使用虚拟mac地址,因为路由问题,可能导致原本的IP不可用
        authentication {            #认证
            auth_type PASS          #认证方式
            auth_pass 1111          #认证密码
        }
        virtual_ipaddress {         
            192.168.226.100                #虚拟的VIP地址
        }
    }

    配置backup

    [root@lb02 ~]# vim /etc/keepalived/keepalived.conf
    global_defs {
        router_id lb02
    }
    
    vrrp_instance VI_1 {
        state BACKUP        
        interface ens160
        virtual_router_id 50
        priority 100
        advert_int 1
        authentication {    
            auth_type PASS
            auth_pass 1111
        }
        virtual_ipaddress {
            192.168.226.100
        }
    }

    image-20260829171313702

    image-20260829171406931

    1. 需要关闭防火墙/放行协议的原因

    Keepalived 的核心是 VRRP 协议,它通过多播(224.0.0.18)或单播发送心跳包来让主备之间互相检测存活状态。

    • 如果防火墙阻挡了 VRRP 报文:
      • MASTER 收不到 BACKUP 的报文 → 不知道对方存在
      • BACKUP 收不到 MASTER 的报文 → 以为 MASTER 挂了,自己也变成 MASTER
      • 结果:两台都认为自己该持有 VIP,出现 “脑裂”,或者 VIP 无法正常漂移
    • 所以要放行:要么彻底关防火墙,要么放行 VRRP协议(协议号 112)。

    工作原理:

    1. 启动时,MASTER(.10)优先级更高,抢占并绑定 VIP
    2. BACKUP(.20)检测到 MASTER 存活,主动放弃 VIP,处于待命状态
    3. 只有 MASTER 对外提供服务,BACKUP 只负责监听心跳
    4. 如果 MASTER 宕机,BACKUP 才会接管 VIP,保证服务不中断

    所以你看到 .10 有 192.168.226.100,.20 没有 → 这就是高可用的正常状态。

    关闭服务器进行抢占

    image-20260829171653605

    image-20260829171845877

    • 配置非抢占式
      • 两个节点的state都必须配置为BACKUP
      • 两个节点都必须加上配置 nopreempt
      • 其中一个节点的优先级必须要高于另外一个节点的优先级。
      • 两台服务器的角色状态都启用nopreempt后,必须修改角色状态统一为BACKUP,唯一的区分就是优先级。
    Master配置
        vrrp_instance VI_1 {
            state BACKUP
            priority 150
            nopreempt
        }
    
    Backup配置
        vrrp_instance VI_1 {
            state BACKUP
            priority 100
            nopreempt
        }

    image-20260829172930996

    非抢占式的影响:

    场景 行为
    .10 宕机 → .20 接管 VIP ✅ 正常切换
    .10 恢复上线 ❌ 不会抢回 VIP(因为 nopreempt)
    VIP 会一直留在 .20 上 直到 .20 也宕机,VIP 才回到 .10

    .10 关了,.20 会在 3 秒内自动接管 VIP,服务不受影响。.10 恢复后,因为配置了nopreempt,不会抢回 VIP,VIP 继续留在 .20 上,直到 .20 也出问题。这是非抢占模式的设计行为。

    image-20260829173533809


    高可用keepalived故障脑裂

    由于某些原因,导致两台keepalived高可用服务器在指定时间内,无法检测到对方的心跳,各自取得资源及服务的所有权,而此时的两台高可用服务器又都还活着。

    脑裂故障原因

    • 服务器网线松动等网络故障
    • 服务器硬件故障发生损坏现象而崩溃
    • 主备都开启firewalld防火墙

    解决脑裂故障方案

    • 如果发生脑裂,则随机kill掉一台即可
    • 在backup上编写检测脚本, 测试如果能ping通master并且backup节点还有vIP的话则认为产生了脑裂
    # 检测脑裂脚本
    [root@server2 ~]# cat check_split_brain.sh
    #!/bin/sh
    vip=192.168.226.100
    lb01_ip=192.168.226.10
    
    while true;do
        ping -c 2 $lb01_ip &>/dev/null
        if [ $? -eq 0 -a `ip add|grep "$vip"|wc -l` -eq 1 ];then
            echo 'ha is split brain.warning!!!' `date`>> /root/check_split_brain.sh.out
    
            systemctl stop keepalived
        else
            echo "ha is ok..." >> /root/check_split_brain.sh.out
        fi
        sleep 5
    done
    [root@server2 ~]# chmod +x check_split_brain.sh 
    [root@server2 ~]# /root/check_split_brain.sh &
    [root@server2 ~]# tail -f check_split_brain.sh.out

    为什么 cat 看到既有 warning 又有 ok?

    text

    ha is split brain.warning!!! 2026年 08月 29日 星期六 18:05:42 CST
    ha is ok...
    ha is ok...
    ha is ok...
    ha is ok...
    时间 状态 输出
    18:05:42 脑裂时 ha is split brain.warning!!!
    18:05:47 修复后(VIP 释放) ha is ok...
    18:05:52 继续检查 ha is ok...
    … … …

    你的理解完全正确:脚本一直在 ping,当条件不满足时就输出 ok。本质变化是 VIP 从 .226.20 上消失了,所以条件从 true 变成了 false。


    一图总结

    text

    循环开始
        ↓
    ping MASTER (192.168.226.10)
        ↓
    ┌─────────────────────────────────┐
    │ 能通? AND 自己有 VIP?          │
    └─────────────────────────────────┘
        ↓                    ↓
       YES                  NO
        ↓                    ↓
     脑裂!              正常!
        ↓                    ↓
    systemctl stop      echo "ha is ok..."
    keepalived
    (自杀,释放VIP)
        ↓
    VIP 消失 → 下次循环条件不成立 → 输出 ok

    image-20260829190422125

    image-20260829190521633


    Keepalived+Nginx实现网站高可用方案

    VIP:192.168.226.100
    lb01:192.168.226.10:8080
    lb02:192.168.226.20:8080
    web01:192.168.226.30:80
    web02:192.168.226.50:80
    

    首先部署web01和web02的网站(以web01为例)

    [root@server3 ~]# yum install nginx -y
    [root@server3 ~]# echo "web01 ..." > /usr/share/nginx/html/index.html 
    [root@server3 ~]# nginx
    # 访问web01、web02测试
    [root@server5 ~]# curl 192.168.226.30
    web01 ...
    [root@server5 ~]# curl 192.168.226.50
    web02 ...

    image-20260829210003209

    部署lb01和lb02(以lb01为例)

    [root@server1 ~]# yum install nginx -y
    [root@server1 ~]# yum install nginx-mod-stream -y
    [root@server1 ~]# vim /etc/nginx/nginx.conf
    include /etc/nginx/conf.c/*.conf;
    [root@server1 ~]# mkdir  /etc/nginx/conf.c
    [root@server1 ~]# vim /etc/nginx/conf.c/lb_domian.conf
    stream {
            upstream lb {
                    server 192.168.226.30:80 weight=5 max_fails=3 fail_timeout=30s;
                    server 192.168.226.50:80 weight=5 max_fails=3 fail_timeout=30s;
            }
    
            server {
                    listen 8080;
                    proxy_connect_timeout 3s;
                    proxy_timeout 3s;
                    proxy_pass lb;
            }
    
    }
    [root@server1 ~]# nginx -t
    nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
    nginx: configuration file /etc/nginx/nginx.conf test is successful
    [root@server1 ~]# nginx
    [root@server1 ~]# ss -tnl
    State    Recv-Q   Send-Q     Local Address:Port       Peer Address:Port   Process   
    LISTEN   0        511              0.0.0.0:8080            0.0.0.0:*                
    LISTEN   0        128              0.0.0.0:22              0.0.0.0:*                
    LISTEN   0        511              0.0.0.0:80              0.0.0.0:*                
    LISTEN   0        128                 [::]:22                 [::]:*                
    LISTEN   0        511                 [::]:80                 [::]:* 
    
    

    部署成功:轮询算法

    image-20260829212355319

    部署keepalived

    [root@server1 ~]# yum install keepalived -y
    [root@server1 ~]# vim /etc/keepalived/keepalived.conf 
    global_defs {                   #全局配置
        router_id lb01              #标识身份->名称
    }
    
    vrrp_instance VI_1 {
        state MASTER                #标识角色状态
        interface ens160             #网卡绑定接口
        virtual_router_id 50        #虚拟路由id
        priority 150                #优先级
        advert_int 1                #监测间隔时间
        authentication {            #认证
            auth_type PASS          #认证方式
            auth_pass 1111          #认证密码
        }
        virtual_ipaddress {
            192.168.226.100                #虚拟的VIP地址
        }
    }
    
    [root@server1 ~]# systemctl start keepalived.service 
    
    [root@server2 ~]# vim /etc/keepalived/keepalived.conf 
    global_defs {                   #全局配置
        router_id lb02              #标识身份->名称
    }
    
    vrrp_instance VI_1 {
        state BACKUP                #标识角色状态
        interface ens160             #网卡绑定接口
        virtual_router_id 50        #虚拟路由id
        priority 100                #优先级
        advert_int 1                #监测间隔时间
        authentication {            #认证
            auth_type PASS          #认证方式
            auth_pass 1111          #认证密码
        }
        virtual_ipaddress {
            192.168.226.100                #虚拟的VIP地址
        }
    }
    [root@server2 ~]# systemctl start keepalived.service

    访问VIP进行测试

    [root@server5 ~]# for i in {1..6};do curl 192.168.226.100:8080;done
    web01 ...
    web02 ...
    web01 ...
    web02 ...
    web01 ...
    web02 ...
    

    image-20260902121609152

    当lb01、lb02产生宕机的时候,keepalived应该可以检测到并发生飘移

    bash
    #!/bin/bash
    
    # 获取当前 Nginx 进程数量(正常是 9,挂了是 0)
    nginx_process_number=`ps -C nginx --no-header | wc -l`
    
    # 判断:如果进程数等于 0,说明 Nginx 挂了
    if [ $nginx_process_number -eq 0 ]; then
        # 尝试重启 Nginx
        systemctl restart nginx
    
        # 重启后再次检查进程数
        nginx_process_number=`ps -C nginx --no-header | wc -l`
    
        # 如果还是 0(重启失败)
        if [ $nginx_process_number -eq 0 ]; then
            exit 1    # 返回 1 → Keepalived 触发 VIP 漂移
        else
            exit 0    # 返回 0 → 正常,不切换
        fi
    else
        # Nginx 正常运行(进程数 = 9),不做任何事
        exit 0
    fi
    

    这个脚本的作用是:==检测 Nginx是否正常运行,如果挂了就尝试重启,重启失败则通知 Keepalived 进行 VIP 漂移。==

    image-20260902132216713

    bash
    # 1. 查看 Nginx 正常运行时的进程数
    [root@localhost ~]# ps -C nginx --no-header | wc -l
    9                              # 1个master + 8个worker = 9个进程 ✅
    
    # 2. 停止 Nginx
    [root@localhost ~]# nginx -s stop
    [root@localhost ~]# ps -C nginx --no-header | wc -l
    0                              # Nginx 已停止,进程数为 0 ✅
    
    # 3. 强制杀掉所有 Nginx 进程
    [root@localhost ~]# pkill nginx
    [root@localhost ~]# ps -C nginx --no-header | wc -l
    0                              # 全部被杀掉,进程数为 0 ✅
    
    # 4. 重新启动 Nginx
    [root@localhost ~]# nginx
    [root@localhost ~]# ps -C nginx --no-header | wc -l
    9                              # 恢复正常,9个进程 ✅
    
    # 5. 给脚本添加执行权限
    [root@localhost ~]# chmod +x /usr/local/src/check_nginx_pid.sh

    在配置文件里配置你的脚本路径

    [root@server1 ~]# vim /etc/keepalived/keepalived.conf
    global_defs {                   #全局配置
        router_id lb01              #标识身份->名称
    }
    
    vrrp_script check_web {
        script "/usr/local/src/check_nginx_pid.sh"       -> # 这里是增加脚本路径
        interval 5
    }
    
    vrrp_instance VI_1 {
        state MASTER                #标识角色状态
        interface ens160             #网卡绑定接口
        virtual_router_id 50        #虚拟路由id
        priority 150                #优先级
        advert_int 1                #监测间隔时间
        track_script {                             # ->  这里是添加脚本调用
        check_web
        }
        authentication {            #认证
            auth_type PASS          #认证方式
            auth_pass 1111          #认证密码
        }
        virtual_ipaddress {         
            192.168.226.100                #虚拟的VIP地址
        }
    }
    

    1. 健康检查脚本定义

    text

    vrrp_script check_web {
        script "/usr/local/src/check_nginx_pid.sh"
        interval 5
    }
    • script:指定检测 Nginx 是否存活的脚本路径
    • interval 5:每 5 秒执行一次脚本
    • 作用:脚本返回 0 表示 Nginx 正常,非 0 表示故障

    2. 脚本调用与认证

    text

    track_script {
        check_web
    }
    authentication {
        auth_type PASS
        auth_pass 1111
    }
    • track_script:引用上面定义的 check_web 脚本,若脚本检测失败则触发 VIP 漂移
    • authentication:主备节点间通信的认证方式(密码必须一致)

    image-20260902132437310

    脚本状态对应表

    Nginx 状态 进程数 脚本执行 返回值 Keepalived 动作
    正常运行 9 走 else 分支 exit 0 不做任何事,保持 MASTER
    挂了但重启成功 0 → 9 走 if 分支,重启后恢复 exit 0 不做任何事,保持 MASTER
    挂了且重启失败 0 → 0 走 if 分支,重启后还是 0 exit 1 VIP 漂移到备机

    image-20260902151200213

    image-20260902151304663


    Keepalived高可用性与代理的关联

    Keepalived与代理(如 Nginx、HAProxy、LVS)通常组合使用,解决互联网服务中的==单点故障==问题。


    1. 各自核心职责

    组件 核心职责 技术本质
    Keepalived 对外保持同一个虚拟IP(VIP),保证入口地址始终可用。 高可用(HA):通过 VRRP 协议检测代理服务是否存活。如果主代理宕机,立即将 VIP 漂移到备用节点。
    代理(Nginx/HAProxy/LVS) 接收客户端请求,根据负载均衡算法转发给后端 Real Server。 负载均衡(LB):将流量分发到多台后端服务器,提高吞吐量和容错能力。

    2. 组合使用的核心原因

    只有Nginx代理而没有 Keepalived:

    • 所有流量经过单台 Nginx。
    • 该Nginx服务器宕机、关机或网络中断时,整个网站无法访问(==单点故障==)。

    只有 Keepalived 而没有代理:

    • Keepalived 本身不处理业务请求,仅提供可漂移的 IP。
    • 请求到达 VIP 后,如果没有代理层接收并转发,流量无法进入后端业务系统。

    组合后的效果:

    Keepalived 为代理层提供高可用保障,确保入口不中断;代理为后端应用提供负载均衡,确保处理能力充足。

    3. 工作流程

    text

    1. 用户访问域名
           |
           ▼
    2. DNS 解析指向虚拟 IP(VIP)
           |
           ▼
    3. Keepalived Master 节点持有该 VIP
           |     (通过 VRRP 协议周期性检测 Master 存活状态)
           ▼
    4. 代理(Nginx)监听 80/443 端口
           |     (配置了 upstream 后端服务器池)
           ▼
    5. 代理根据权重/轮询等算法,将请求转发给后端 Web Server

    4. 故障转移机制

    当 Master 节点发生故障时:

    1. Keepalived 检测:Backup 节点在 3 个心跳周期内(约 3 秒)未收到 Master 的 VRRP 广播包。
    2. VIP 漂移:Backup 节点立即接管 VIP。
    3. 代理接管:新请求通过 VIP 到达 Backup 节点的代理服务。
    4. 业务无感:Backup 节点的代理继续将请求转发给后端服务器池。

    对用户而言,网站入口地址不变,服务未中断。

    5. 常见组合方式

    组合方式 优势 适用场景
    LVS + Keepalived 性能极高,四层转发,无数据拷贝,抗DDoS 能力强 大型门户、直播平台、TCP 四层入口
    Nginx + Keepalived 配置灵活,支持七层 HTTP 高级特性(正则、缓存)、SSL 卸载 Web 应用、微服务网关、API 管理
    HAProxy + Keepalived 支持四层和七层负载,健康检查功能强大 高要求 TCP/HTTP 负载均衡场景

    关联总结

    Keepalived 保证代理服务的高可用,确保入口 IP不丢失;代理负责请求分发,保证流量能到达后端业务服务器。两者协同工作,共同保障网站入口的 7×24 小时不间断服务。


    LVS的NAT模式

    当用户访问服务器集群提供的服务时,发往虚拟IP地址的请求数据包(负载均衡器的外部IP地址)到达负载均衡器。 负载均衡器检查数据包的目标地址和端口号。如果根据虚拟服务器规则表匹配虚拟服务器服务,则通过调度算法从集群 中选择 真实服务器,并将连接添加到记录已建立连接的哈希表中。然后,将目标地址和数据包的端口重写为所选服务器 的目标地址和端口,并将数据包转发到服务器。当传入数据包属于此连接并且可以在哈希表中找到所选服务器时,将重写 该数据包并将其转发到所选服务器。当回复数据包返回时,负载均衡器将数据包的源地址和端口重写为虚拟服务的源地址 和端口。连接终止或超时后,连接记录将在哈希表中删除。

    LVS的NAT模式特点: RS应该使用私有地址,网关为DIP DIP和RIP属于同一个网段 请求和响应报文都需要经过DS(性能瓶颈) 支持端口映射 RS可以使用任意操作系统

    一、整体流程:一个请求的”NAT之旅”

    当用户访问服务器集群提供的服务时,发往虚拟IP地址的请求数据包到达负载均衡器。整个过程可以拆解为以下几个关键步骤:

    步骤 方向 操作 数据包详情(基于 192.168.226.xxx)
    ① 客户端 → DS 用户访问 VIP 客户端 192.168.226.128:12345 → VIP 192.168.226.100:80
    ② DS内部 匹配虚拟服务规则 DS检查规则表,命中 192.168.226.100:80 的服务
    ③ DS 内部 调度算法选 RS 通过轮询等算法选中 RS1(192.168.226.30:80),并在哈希表中记录该连接
    ④ DS → RS1 DNAT 转换 目标地址改写为 192.168.226.30:80,转发给RS1
    ⑤ RS1 内部 处理业务 RS1 处理请求,生成响应数据
    ⑥ RS1 → DS 响应包先回 DS RS1 的响应包 192.168.226.30:80 → 客户端 192.168.226.128:12345,==因网关指向 DIP==(192.168.226.10),先回到 DS
    ⑦ DS → 客户端 SNAT 转换 源地址改写为 192.168.226.100:80 → 客户端 192.168.226.128:12345

    当后续同一个连接的数据包到达时,DS可以在哈希表中查找到之前记录的 RS,直接将数据包转发给对应的 RS,无需重新调度。连接终止或超时后,连接记录在哈希表中删除。

    在这个过程中,DS扮演了一个”网络地址转换器”的角色,对外屏蔽了内部的真实服务器结构。客户端全程只与 VIP 交互,感知不到后端 RS 的存在。

    在 192.168.226.x 网段的环境里:

    • DS这台机器本身有一个真实 IP(DIP),比如 192.168.226.10,用于和后端服务器通信。
    • DS上同时还配置了一个 VIP,比如 192.168.226.100,这个 IP 是对外公布的服务地址,客户端只认这个地址。

    关键点:客户端访问 VIP → VIP 就在 DS上 → DS 直接收到包 →DS 做 DNAT→ 转发给 RS。

    流程图

    text

    客户端 (192.168.226.128)
        |
        | 访问 http://192.168.226.100:80
        ↓
    ┌─────────────────────────────────┐
    │   DS(负载均衡器)               │
    │   网卡上配置了两个地址:          │
    │   ├── VIP: 192.168.226.100     │  ← 客户端访问的就是这个
    │   └── DIP: 192.168.226.10      │  ← 用来和 RS 通信
    │                                 │
    │   收到包后,做 DNAT:            │
    │   目标地址 192.168.226.100:80   │
    │   → 改为 192.168.226.30:80      │
    └─────────────────────────────────┘
        |
        | 转发给 RS1
        ↓
    ┌─────────────────────────────────┐
    │   RS1 (192.168.226.30)           │
    │   处理请求,响应包通过网关        │            #   RS 的网关 = DS(的 DIP 地址),这是让响应包“回家”做地址转换的
    │  (192.168.226.10) 回到 DS      │                唯一通道
    └─────────────────────────────────┘

    核心配置要点(必须满足的条件)

    基于 192.168.226.x 网段,NAT 模式有以下硬性要求:

    RS 使用私有地址,且网关指向 DIP

    角色 IP地址 网关设置
    DS(DIP) 192.168.226.10 —
    RS1(RIP) 192.168.226.30 默认网关必须指向 192.168.226.10
    RS2(RIP) 192.168.226.40 默认网关必须指向 192.168.226.10
    • 为什么? 请求包经过 DS 后,目标 IP 变成了 RS 的私有 IP(如 192.168.226.30)。RS 处理完请求后,响应包要回给客户端(192.168.226.128)。但 RS 自己不知道客户端在哪,所以它的默认网关必须指向 DS 的 DIP。只有这样,响应包才会先回到DS,由DS 做 SNAT 后发出。
    • 若不满足会怎样? RS 可能会把响应包直接丢给它的默认网关(比如路由器),而这个路由器不认识这个连接,就会丢包,导致访问失败。

    DIP 和 RIP 属于同一个网段

    在我们的环境里,DIP(192.168.226.10)和所有 RIP(192.168.226.30、192.168.226.40)都在 192.168.226.0/24 网段内。

    • 为什么? DS改写目标地址后,要通过二层交换(MAC 地址) 把包发给 RS。如果 DIP 和 RIP 不在同一网段,DS 就需要通过路由转发,这会增加复杂度和延迟。

    支持的额外特性

    • 支持端口映射:例如客户端访问 192.168.226.100:8080,DS 可以将其映射到 RS1 的 192.168.226.30:80,实现内外端口不一致。
    • RS 操作系统无关:RS 可以是 Windows、Linux、Unix 等任何操作系统,只要能正常处理 TCP/IP 协议即可。

    LVS的DR模式

    一、整体流程:一个请求的 “DR 之旅”

    DR 模式的核心思想是:请求走 DS,响应直接回客户端,从而避免 NAT 模式中响应流量也经过 DS 的性能瓶颈。

    在 192.168.226.x 网段环境下,一个请求的完整流程如下:

    步骤 方向 操作 数据包详情(基于192.168.226.xxx)
    ① 客户端 → DS 用户访问 VIP 客户端 192.168.226.128:12345 → VIP 192.168.226.100:80
    ② DS 内部 匹配虚拟服务规则 DS 检查规则表,命中 192.168.226.100:80 的服务
    ③ DS内部 调度算法选 RS 通过调度算法选中RS1(192.168.226.30),在哈希表中记录连接
    ④ DS → RS1 修改 MAC 地址 DS 将数据帧的目标 MAC 地址改为 RS1 的 MAC 地址,在 LAN 上重新传输。数据包的 IP 地址不变(目标 iP 仍是 192.168.226.100)
    ⑤ RS1 内部 处理请求 RS1 的 lo 接口上配置了 VIP(192.168.226.100),所以它能接收并处理目标为 VIP 的请求
    ⑥ RS1 → 客户端 响应直接返回 RS1将响应包源地址设为 192.168.226.100,直接通过网关(路由器 192.168.226.1)发回给客户端,不经过DS

    关键区别:DR 模式中,DS 只修改了数据帧的 MAC 地址(在二层工作),而没有修改 IP 地址和端口(没有DNAT)。RS 收到的请求包目标 IP仍然是 VIP,因为 RS 的 lo 接口上也配置了 VIP,所以它能正常处理这个包。

    关键校验:

    • ✅ DS 和所有 RS 在同一个二层网络(LAN)中,通过交换机/HUB 相连。
    • ✅ RS 的默认网关指向路由器 192.168.226.1,绝不能指向 DIP(192.168.226.10)。
    • ✅ RS 的 lo 接口上配置了 VIP(192.168.226.100),并设置了 ARP抑制。

    二、DR 模式的核心机制详解

    1. 为什么 DR 模式只需要改 MAC 地址?

    在 NAT 模式中,DS 做 DNAT把目标 IP从 VIP 改成 RIP,所以响应包必须先回DS做 SNAT。

    但在 DR 模式中:

    • DS不改IP地址,只把数据帧的目标 MAC 地址从 DS 的 MAC 改成 RS 的 MAC。
    • RS 收到的包:目标 IP 仍然是 192.168.226.100(VIP),但因为 RS 的 lo 接口上也配置了 192.168.226.100,所以 RS 的 TCP/IP 协议栈认为这个包是发给自己的,正常接收并处理。
    • RS 响应时,源地址直接设为 192.168.226.100,因为客户端访问的就是这个 IP,所以客户端能正确识别响应包。

    text

    客户端 192.168.226.128
        |
        | 请求:目标 IP = 192.168.226.100
        ↓
    ┌──────────────────────────────────────────────────┐
    │  DS(负载均衡器)                                │
    │  ┌──────────────────────────────────────────┐   │
    │  │  收到请求包:                            │   │
    │  │  源 IP: 192.168.226.128                  │   │
    │  │  目标 IP: 192.168.226.100 (VIP)         │   │
    │  │  目标 MAC: DS 的 MAC                    │   │
    │  └──────────────────────────────────────────┘   │
    │  调度算法选中 RS1                               │
    │  ↓ 只改 MAC,不改 IP                          │
    │  ┌──────────────────────────────────────────┐   │
    │  │  转发包:                                │   │
    │  │  源 IP: 192.168.226.128 (不变)           │   │
    │  │  目标 IP: 192.168.226.100 (VIP, 不变)   │   │
    │  │  目标 MAC: 改为 RS1 的 MAC              │   │
    │  └──────────────────────────────────────────┘   │
    └──────────────────────────────────────────────────┘
        ↓
    ┌──────────────────────────────────────────────────┐
    │  RS1(物理网卡 IP: 192.168.226.30)              │
    │  lo 接口别名: 192.168.226.100                   │
    │  ┌──────────────────────────────────────────┐   │
    │  │  收到包,检查目标 IP: 192.168.226.100   │   │
    │  │  → lo 上有这个 IP,收下!               │   │
    │  │  处理请求,生成响应                      │   │
    │  └──────────────────────────────────────────┘   │
    │  ↓ 响应直接发,不经过 DS                        │
    │  源 IP: 192.168.226.100 (VIP)                  │
    │  目标 IP: 192.168.226.128                       │
    └──────────────────────────────────────────────────┘
        ↓
    直接到达客户端 192.168.226.128 ✅

    ==本质==:

    有相同的 VIP → 能直接通信 → 绕过 DS”

    1. “有相同的 VIP” → RS 的 lo 接口上配置了和 DS一样的 VIP,所以 RS 认为自己也是这个 IP的“主人”。
    2. “能直接通信” → RS 收到请求后,直接以 VIP 的身份响应客户端,不需要经过 DS转手。
    3. “绕过 DS” → 响应流量完全不经过DS,减轻了 DS的压力,这就是 DR 模式性能高的根本原因

    ARP 抑制(最关键的一步!)

    为什么需要 ARP抑制?

    在 192.168.226.x 网段中,DS 和所有 RS 的 lo 接口都配置了 VIP(192.168.226.100)。如果不做任何限制:

    • 当路由器 192.168.226.1 询问“谁有 192.168.226.100 的 MAC 地址?”时,DS 和所有 RS 都会响应。
    • 路由器可能把 VIP 的 MAC 地址映射到某一台 RS 上,导致部分请求绕过 DS直接发给 RS,破坏了负载均衡。

    2、 ARP 抑制的作用(核心:防止“喧宾夺主”)

    在LVS-DR 模式中,DS(调度器)和 RS(真实服务器)都配置了同一个 VIP。如果不做处理,当客户端发送ARP请求询问“谁是 VIP?”时,RS 也会响应,导致客户端直接把请求发给 RS,==DS 就失去了负载均衡的作用==。

    ARP 抑制的具体作用:

    1. 隐藏 RS 的 VIP:让 RS 在网络上“装作不知道”自己有这个 VIP,不响应外部对 VIP 的 ARP 请求。
    2. 确保流量入口唯一:保证只有 DS 会响应 VIP 的 ARP请求,从而确保所有客户端的请求必须先经过 DS。
    3. 实现原理:通常通过修改 Linux 内核参数(arp_ignore 和 arp_announce)来实现,让 RS 只在特定的 lo 接口上识别 VIP,而不在物理网卡上对外宣告 VIP。

    3、LVS-DR 模式核心总结(本质与流程)

    DR 模式的本质可以总结为以下三点:

    1. 请求必须经过 DS(入口在 DS)
      • 客户端请求到达DS,DS 根据调度算法选择一台 RS。
      • 关键动作:DS只修改目标 MAC 地址(改为 RS 的 MAC),不修改源IP和目标IP(目标 IP 依然是 VIP)。
      • DS 将数据包转发给选中的 RS。
    2. 响应不经过DS(出口在 RS)
      • RS 收到数据包后,发现目标IP是自己 lo 接口上的 VIP,于是接收并处理请求。
      • 关键动作:RS 处理完毕后,直接以 VIP 为源IP,客户端IP 为目标IP,将响应包直接发送给客户端。
      • 响应流量完全不经过 DS,这就是 DR 模式性能极高的根本原因(DS 只处理入站请求,不处理出站响应,吞吐量极大提升)。
    3. ARP 抑制是前提(保障机制)
      • 正是因为有了ARP 抑制,才保证了“请求先到 DS”这一前提。
      • 如果没有ARP抑制,客户端可能直接找到 RS,DS 就形同虚设。

    LVS的TUN模式

    在LVS(NAT)模式的集群环境中,由于所有的数据请求及响应的数据包都需要经过LVS调度器转发,如果后端服务器的数量大于10台,则调度器就会成为整个集群环境的瓶颈。我们知道,数据请求包往往远小于响应数据包的大小。因为响应数据包中包含有客户需要的具体数据,所以LVS(TUN)的思路就是将请求与响应数据分离,让调度器仅处理数据请求,而让真实服务器响应数据包直接返回给客户端。IP隧道(IP tunning)是一种数据包封装技术,它可以将原始数据包封装并添加新的包头(内容包括新的源地址及端口、目标地址及端口),从而实现将一个目标为调度器的VIP地址的数据包封装,通过隧道转发给后端的真实服务器(Real Server),通过将客户端发往调度器的原始数据包封装,并在其基础上添加新的数据包头(修改目标地址为调度器选择出来的真实服务器的IP地址及对应端口),LVS(TUN)模式要求真实服务器可以直接与外部网络连接,真实服务器在收到请求数据包后直接给客户端主机响应数据。

    一、整体流程:一个请求的 “TUN 之旅”

    text

    客户端 (CIP)
      |
      | 请求:目标 IP = VIP
      ↓
    [ DS (调度器) ]
      |
      | 1. 收到请求包(目标 IP 是 VIP)
      | 2. 调度算法选中 RS1
      | 3. 关键动作:IP 隧道封装
      |    - 在原始数据包外,再加一层新的 IP 包头
      |    - 新目标 IP = RS1 的 RIP (192.168.226.30)
      |    - 新源 IP = DS 的 DIP (192.168.226.100)
      |
      | 封装后的包: [ 新 IP 头 (DIP -> RIP) ] + [ 原始 IP 头 (CIP -> VIP) + 数据 ]
      ↓
    [ RS1 (真实服务器) ]
      |
      | 1. 收到封装包,发现新目标 IP 是自己(RIP),于是拆开外层包头
      | 2. 看到内层原始包头,目标 IP 是 VIP,而自己的 lo 接口上配置了 VIP
      | 3. 接收并处理请求,生成响应
      |
      | 4. 响应:直接通过物理网卡发回给客户端
      |    - 源 IP = VIP (192.168.226.100)
      |    - 目标 IP = CIP (192.168.226.128)
      ↓
    直接到达客户端 (CIP) ✅

    二、 TUN 模式的核心机制详解

    1. 为什么 TUN 模式需要 IP隧道封装?

    在 DR 模式中,DS 和 RS 必须在同一个物理网段(二层网络),因为 DS 是通过修改 MAC 地址 来转发数据包的。 但在 TUN 模式中,DS 和 RS 可以跨越不同的网段甚至不同的机房(三层网络)。因为无法直接修改 MAC 地址,DS必须使用 IP隧道技术(IPIP):

    • DS端:把原始数据包(目标IP是 VIP)整个作为数据,再在外面套上一个新的 IP 包头(目标IP 是 RS 的 RIP),然后通过路由发出去。
    • RS 端:收到包后,剥掉外层 IP包头,露出里面的原始数据包。发现目标 IP是 VIP,且自己的 lo 接口上有 VIP,于是正常处理。

    2. RS 响应时如何直接回给客户端?

    与 DR 模式完全一致:

    • RS 处理完请求后,源 IP 设为 VIP(因为客户端请求的就是这个IP),目标IP 设为客户端 IP。
    • RS 通过自己的物理网卡,直接将该响应包发送给客户端(或通过 RS 自己的网关路由出去),完全不经过 DS。

    3. 隧道机制的关键要求

    • RS 必须支持IP隧道协议:RS 的操作系统内核必须支持 IPIP 隧道(Linux 默认支持,通过 modprobe ipip 加载模块)。
    • RS 的 lo 接口必须配置 VIP:这是让 RS 认为“这个包是发给我的”的关键,也是实现响应源 IP 为 VIP 的前提。
    • RS 的网关不能指向DS:因为响应包要直接发给客户端,如果网关指向DS,响应包就会先发给DS,违背了“响应不经过DS”的初衷,DS又会成为瓶颈。

    三、 TUN 模式的核心配置要点(必须满足的条件)

    1. VIP 配置:DS 的物理网卡上配置 VIP;RS 的 lo 接口上配置 VIP(通常需要做ARP 抑制,虽然 TUN 模式下DS和 RS 可能不在同一网段,ARP 影响较小,但配置 lo 依然是标准做法)。
    2. 隧道配置:DS 和 RS 都需要配置IPIP 隧道(如 ip tunnel add tunl0 mode ipip),并分配隧道 IP。
    3. 路由配置:DS需要将发往 RS 的包通过隧道路由出去;RS 需要将响应包通过物理网卡路由给客户端(不能经过 DS)。
    4. RS 网关:RS 的默认网关绝对不能指向DS的 DIP,必须指向自己所在网段的真实网关(或直接配置去往客户端网段的路由)。

    四、 TUN 模式支持的额外特性

    • 跨网段部署:DIP、VIP、RIP 可以是公网地址,且可以分布在不同的物理网段,甚至跨越机房(这是 TUN 最大的优势)。
    • 请求报文经由 Director,响应不经由 Director:与 DR 模式一样,实现了请求与响应的分离,极大提升了集群的吞吐量。
    • 不支持端口映射:与 DR 模式一样,DS 不修改端口,因此无法做端口映射(如把 80 端口映射到 8080)。
    • RS 的 OS 须支持隧道功能:这是 TUN 模式特有的硬性要求。

    五、 LVS-TUN 模式核心总结(本质与流程)

    1. 请求必须经过 DS(入口在 DS)
      • 客户端请求到达 DS,DS根据调度算法选择一台 RS。
      • 关键动作:DS不修改原始 IP 包头,而是在原始包外再封装一层新的IP包头(新目标IP 是 RS 的 RIP),通过隧道发给 RS。
    2. 响应不经过DS(出口在 RS)
      • RS 收到封装包,剥离外层包头,发现内层目标IP 是 VIP(自己的 lo 上有),于是处理请求。
      • 关键动作:RS 处理完毕后,直接以 VIP 为源IP,客户端 IP为目标 IP,将响应包直接发送给客户端。
      • 响应流量完全不经过DS,减轻了 DS 的压力。
    3. ARP抑制与隧道机制
      • ARP 抑制在 TUN 模式中依然重要(防止 RS 在物理网络上响应 VIP 的ARP),但 TUN 模式更核心的是 IP隧道封装,它解决了 DR 模式不能跨网段的限制。

    一句话终极总结():

    “TUN 模式用 IP 隧道封装突破网段限制,DS 只负责加个壳转发请求,RS 拆壳处理后直接回包给客户端;请求走 DS,响应走 RS,性能高且能跨机房。”


    LVS-DR与 LVS-TUN 终极对比总结

    本质定性: TUN 模式本质上是 DR 模式在跨网段场景下的“升级版”。两者的核心思想完全一致,即“请求经过DS,响应不经过 DS”,从而实现请求与响应的分离,极大提升集群吞吐量。

    唯一的核心区别(DS如何转发请求):

    • DR 模式(二层转发):在数据链路层修改目标 MAC 地址(改为 RS 的 MAC),要求 DS 与 RS 必须在同一物理网段(二层互通)。
    • TUN 模式(三层转发):在网络层给原始数据包加一层新的IP 包头(IP 隧道封装,目标 IP为 RS 的 RIP),允许 DS与 RS 在不同网段(三层互通,可跨机房)。

    核心原理全一样,请求过DS,响应不过 DS;区别只在怎么转,DR 改 MAC 限同网段,TUN 套 IP 可跨网段。”


    LVS的三大类算法

    一、 静态算法(Static Scheduling)

    核心特点:调度器不关心后端服务器当前有多忙、是否宕机,只根据预设的规则(如轮流、权重、源 IP)进行分配。适合无状态、处理时间短且服务器性能相近的服务。

    1. 轮询(Round Robin, RR)

    • 原理:最公平的“排排坐,吃果果”。请求 1 给 A,请求 2 给 B,请求 3 给 C,请求 4 再回到 A。
    • 适用:无状态服务(如静态网页),且所有服务器性能完全一致。
    • 缺点:现实中服务器性能往往不同,性能差的机器容易被压垮。

    2. 加权轮询(Weighted Round Robin, WRR)

    • 原理:给性能好的服务器更高的权重。比如 A(权重3),B(权重1),那么分配序列就是 A, A, A, B, A, A, A, B… 按比例分配。
    • 适用:服务器性能差异较大的场景,通过权重来“能者多劳”。

    3. 源地址哈希(Source IPHash, SH)

    • 原理:对客户端的源IP 进行哈希计算,同一个IP的请求永远固定分配给同一台服务器。
    • 适用:需要会话保持(Session 保持)的场景。比如用户登录后,如果下次请求被分到另一台机器,Session 就丢了,SH 可以解决这个问题。
    • 缺点:如果某个 IP段(比如一个公司出口)疯狂发请求,会导致那台服务器压力过大(负载倾斜)。

    二、 动态算法(Dynamic Scheduling)

    核心特点:调度器会实时监控后端服务器的连接数等状态,动态调整分配。适合处理时间差异大、负载变化快的场景。

    1. 最少连接(Least Connections, LC)

    • 原理:谁的当前连接数最少,就把新请求给谁。这能动态平衡负载。
    • 适用:请求处理时间差异大的场景(如数据库查询、复杂计算),因为处理慢的机器连接数会积累,新请求就不给它了。
    • 缺点:不看服务器性能。一台性能极强但当前连接数稍多的服务器,可能不如一台性能差但连接数少的服务器受青睐。

    2. 加权最少连接(Weighted Least Connections, WLC)

    • 原理:LVS 的默认算法。结合了“权重”和“连接数”。公式:当前连接数 ÷ 权重。计算结果越小,说明该服务器相对越“闲”,优先分配。
    • 例子:A(权重2,连接4,4/2=2),B(权重1,连接1,1/1=1)。B 的值更小,所以新请求给 B。
    • 适用:服务器性能差异大,且请求处理时间不稳定的场景(最通用、最智能的算法之一)。

    3. 基于局部性的最少连接(Locality-Based Least Connections, LBLC)

    • 原理:专为缓存集群(如 CDN、Squid)设计。它优先把请求分配给“已经缓存了目标内容”的服务器。如果都没缓存,就按 LC(最少连接)分配。
    • 适用:缓存服务器集群,目的是提高缓存命中率,减少回源。

    4. 带复制的基于局部性最少连接(LBLCR)

    • 原理:在 LBLC基础上增加了“复制”功能。如果目标内容没在任何服务器上,LBLC只是随便找台机器处理,而 LBLCR 会把内容复制到负载较轻的机器上,并让后续请求都指向这台机器。
    • 适用:大型缓存集群,进一步提升命中率。

    5. 目标地址哈希(DestinationIP Hash, DH)

    • 原理:对请求的目标IP 进行哈希,同一目标 IP 的请求固定给同一台服务器。
    • 适用:正向代理场景(如网关转发到特定后端服务)。

    三、 其他算法(特殊场景优化)

    这些算法在 WLC 和 SED 的基础上,为了减少延迟或避免排队而设计。

    1. 最短预期延迟(Shortest Expected Delay, SED)

    • 原理:基于 (当前连接数 + 1) ÷ 权重 计算。与 WLC 类似,但更激进地倾向于权重高的服务器。
    • 场景:需要优先利用高性能服务器的场景。

    2. 永不队列(Never Queue, NQ)

    • 原理:如果存在空闲服务器(连接数为 0),直接分配给它,不排队。如果都忙,再按 SED 算法分配。
    • 场景:对响应延迟要求极高的场景,避免请求在高负载服务器上排队。

    核心总结与选择指南(速查表)

    算法类型 算法名称 核心逻辑 典型适用场景
    静态 RR 轮流分配 无状态、性能一致的集群
    静态 WRR 按权重轮流 性能差异大、无状态
    静态 SH 源IP哈希 需要 Session 保持
    动态 LC 谁连接少给谁 请求处理时间差异大
    动态 WLC (连接数/权重) 最小优先 最通用,性能差异大且请求时间不稳
    动态 LBLC 优先给有缓存的机器 缓存集群(CDN)
    动态 LBLCR 缓存复制 + 优先给有缓存的 大型缓存集群
    动态 DH 目标IP 哈希 正向代理、网关转发
    其他 SED (连接数+1)/权重 最小优先 优先压榨高性能服务器
    其他 NQ 有空闲机器就上,不等 极低延迟要求,避免排队

    一句话记忆法:

    • 要会话保持用 SH;
    • 要智能动态用 WLC(默认);
    • 要缓存优化用 LBLC/LBLCR;
    • 要极致低延迟用 NQ。

    LVS 的NAT案例

    image-20260914111442797

    LVS之NAT案例
    注意:RS的网关一定要指向DS的仅主机网卡地址
    # 环境准备
    DIP:192.168.195.10/24
    VIP:192.168.226.10/16
    RIP1:192.168.195.20   GATEWAY:192.168.195.10     # GATEWAY是RS网关要指向DS的DIP(物理网卡地址)
    RIP2:192.168.195.30   GATEWAY:192.168.195.10
    # DS部署
    1.安装ipvsadm工具
    [root@server1 ~]# yum install ipvsadm ‐y
    2.打开转发,配置nat
    [root@server1 ~]# echo 1 >> /proc/sys/net/ipv4/ip_forward
    [root@server1 ~]# iptables ‐t nat ‐F
    [root@server1 ~]# iptables ‐t nat -A POSTROUTING -s 192.168.195.0/24 -o ens160 -j MASQUERADE
    3.设置wrr策略和添加RS主机
    [root@server1 ~]# ipvsadm ‐A ‐t 192.168.226.10:80 ‐s wrr
    [root@server1 ~]# ipvsadm ‐a ‐t 192.168.226.10:80 ‐r 192.168.195.11:80 ‐m ‐w 1
    [root@server1 ~]# ipvsadm ‐a ‐t 192.168.226.10:80 ‐r 192.168.195.12:80 ‐m ‐w 1
    4.查看策略信息
    [root@server1 ~]# ipvsadm ‐ln
    IP Virtual Server version 1.2.1 (size=4096)
    Prot LocalAddress:Port Scheduler Flags
    ‐> RemoteAddress:Port Forward Weight ActiveConn InActConn
    TCP 192.168.226.10:80 wrr
    ‐> 192.168.195.11:80 Masq 1 0 0
    ‐> 192.168.195.12:80 Masq 1 0 0
    # RS部署
    # RS的网关必须指向DS
    [root@server2 ~]# echo "RS2" > /usr/share/nginx/html/index.html
    [root@server3 ~]# echo "RS3" > /usr/share/nginx/html/index.html
    [root@server1 ~]# curl 192.168.226.10
    RS1
    [root@server1 ~]# curl 192.168.226.10
    RS2
    注意:selinux策略和防火墙!
    

    一、 核心角色与网卡规划表

    在 LVSNAT 模式中,Director(负载均衡器)需要两块网卡,Real Server(真实服务器)通常只需要一块内网网卡。

    设备名称 角色 网卡名称 VMware 网络模式 IP规划 (示例) 说明
    Director (LVS) 负载均衡器 ens160 NAT 模式 (VMnet8) VIP: 192.168.226.100 (实际IP: 192.168.226.18) 对外网卡。负责接收客户端请求,并作为源IP 将响应发回给客户端。
    Director (LVS) 负载均衡器 ens224 仅主机模式 (VMnet1) DIP: 192.168.195.10 对内网卡。负责将请求转发给后端的 RS,并接收 RS 返回的数据。
    Real Server 1 后端真实服务器 ens160 仅主机模式 (VMnet1) RS1 IP: 192.168.195.11 网关: 192.168.195.10 提供实际服务。网关必须指向 Director 的 DIP。
    Real Server 2 后端真实服务器 ens160 仅主机模式 (VMnet1) RS2 IP: 192.168.195.12网关: 192.168.195.10 提供实际服务。网关必须指向 Director 的 DIP。
    客户端 (Client) 访问请求方 – 与 VIP 连通即可 宿主机或另一台 NAT 模式虚拟机 通过访问 VIP(192.168.226.100)来测试负载均衡。

    二、 配置操作三大核心步骤

    1. VMware 层面设置

    • 虚拟网络编辑器 (图1):
      • VMnet8 (NAT模式):子网设为 192.168.226.0,开启 DHCP。
      • VMnet1 (仅主机模式):子网设为 192.168.195.0,开启 DHCP。
    • 虚拟机设置 (图2):
      • Director 虚拟机:添加两块网卡。“网络适配器”选 NAT,“网络适配器2”选仅主机。
      • RS 虚拟机:只保留一块网卡,选“仅主机模式”。

    2. Linux 系统层面配置(静态 IP)

    • Director 节点:
      • 修改 ens160 配置文件:设置静态IP 192.168.226.18(==实际ip不是DIP==),再添加一个vip:192.168.226.100
      • 修改 ens224 配置文件:设置静态IP 192.168.195.10(不要配默认网关)。
      • 添加 VIP:通过命令 ifconfig ens160:0 192.168.226.100 netmask 255.255.255.0 up 或配置文件添加。
    • RS 节点:
      • 修改网卡配置文件:设置静态IP 192.168.195.11 和 192.168.195.12。
      • 关键: 将它们的 GATEWAY 设置为 192.168.195.10(即 Director 的 DIP)。

    3. Director 系统内核配置

    • 开启 IP 转发(NAT 模式必须):

      bash

      echo 1 > /proc/sys/net/ipv4/ip_forward
      # 永久生效需写入 /etc/sysctl.conf

    三、 核心逻辑总结(一句话记忆)

    • VIP 走 NAT 模式:用来“对外”接收客户端的请求,并伪装成客户端与 RS 通信的源地址。
    • DIP 走仅主机模式:用来“对内”和 RS 建立专属局域网,确保 Director 能直接找到 RS。
    • RS 的网关必须指向 DIP:这是 NAT 模式回包的关键,确保 RS 处理完的数据能原路返回给 Director,再由 Director 发回给客户端。
    • 别让两张网卡在同一网段:DIP 和 VIP 必须处于不同的网段(195 和 226),否则路由器会“精神分裂”,导致 NAT 转发失败。

    1. 为什么一个网卡能有多个IP?

    网卡(如 ens160)在物理层面只是一块硬件,但操作系统可以通过软件的方式,在这块硬件上绑定多个逻辑IP。

    • 一个是主 IP(Primary IP):比如 192.168.226.18,这是网卡自己的真实地址。
    • 其余的是辅助IP/ 别名(Secondary IP/ Alias):比如 192.168.226.100,这就是我们给 LVS配置的 VIP。

    2. 在您的架构中,ens160是怎么拥有两个IP 的?

    在您的 Director(负载均衡器)上,ens160 这块网卡同时绑定了两个 226 网段的 IP:

    • 实际 IP(192.168.226.18):这是 Director 用来和宿主机通信、或者您用来 SSH 登录管理的IP。
    • VIP(192.168.226.100):这是绑定在 ens160 上的虚拟IP。当客户端访问 192.168.226.100 时,数据包会进入 ens160,LVS软件会拦截这个请求并进行转发。

    在终端中看起来是这样的(通过 ip a 命令):

    text

    2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
        link/ether 00:0c:29:72:0b:3d brd ff:ff:ff:ff:ff:ff
        inet 192.168.226.18/24 brd 192.168.226.255 scope global noprefixroute ens160
           valid_lft forever preferred_lft forever
        inet 192.168.226.100/24 scope global secondary ens160   <--- 这里多了一个 VIP
           valid_lft forever preferred_lft forever

    注意看最后一行,多了一个 secondary 标志,这就是同一个网卡上的第二个 IP。

    image-20260912003002246

    image-20260912120941620


    ipvsadm作用

    它是 Linux 内核中LVS(负载均衡)功能的“遥控器”和管理工具。

    具体来说,它的作用只有以下三点:

    1. 配置虚拟服务(VIP):告诉系统对外提供服务的 IP 和端口是什么(比如 192.168.226.100:80)。
    2. 管理后端服务器(RS):把真实的服务器 IP 绑定到这个 VIP 后面,让它们组成一个服务器集群。
    3. 查看和修改调度规则:决定用什么算法(比如轮询)把客户端的请求分发给后端的真实服务器,同时可以随时查看当前的连接状态或删除规则。

    nmtui 的作用

    nmtui 是 Linux 系统中用来配置网络的一个“图形化文本界面”工具。

    (全称是 NetworkManager Text User Interface,即网络管理器的文本用户界面)

    具体来说,它的核心作用有以下三点:

    1. 图形化配置网络:它提供了一个类似 BIOS 那样的蓝底菜单界面。您不需要去死记硬背繁琐的配置文件路径(比如 /etc/sysconfig/network-scripts/ifcfg-ens160),也不需要记复杂的 nmcli 长命令。直接用键盘方向键和回车,就能修改 IP、网关、DNS。
    2. 轻松激活/停用网卡:可以在界面里直接启用或禁用某张网卡,修改完配置后能一键生效,不需要每次敲命令重启整个网络服务。
    3. 管理网络连接:可以添加新的网络连接(比如添加新的 VLAN、绑定网卡等),或者删除不需要的旧连接。

    image-20260912131334067

    image-20260912132023147

    image-20260912132433942

    image-20260912141042053

    核心总结:为什么 RS 设置好后,MobaXterm 无法直接 SSH?

    一句话结论: 因为您把 RS 的网卡配置成了“仅主机模式”,并设置了 195 内网网段 IP。这相当于把 RS 关进了一个没有外网入口的“密室”。而您的 Windows 宿主机(运行 MobaXterm 的电脑)默认处于 226 外网(NAT)网段,两者不在同一个局域网内,网络物理隔离,所以 MobaXterm 的连接请求找不到目标,必然失败。


    详细原因拆解:

    1. IP地址不匹配(根本原因):
      • 您的 MobaXterm 所在宿主机:处于 192.168.226.x 网段(NAT模式)。
      • 您的 RS 服务器:处于 192.168.195.11(仅主机模式)。
      • 这就像您在北京打电话,要找的人在上海,但没有长途线路,根本找不到。
    2. VMware仅主机模式的特性:
      • “仅主机模式(VMnet1)”设计的初衷,就是创建一个封闭的虚拟内网,用来模拟真实机房里的“内网隔离区”。默认情况下,它拒绝外部设备直接访问。
    3. 网卡名字无关:
      • 无论这块网卡叫 ens160 还是 ens224,只要它插在“仅主机模式”的虚拟交换机上,并且配了 195 网段的IP,它就注定无法被外网直接访问。名字只是 Linux 分配的代号,决定连接能不能通的是“IP 网段”和“网络模式”。

    最终定论:

    “RS 配好后 MobaXterm连不上”不是配置错了,而是成功实现了“后端服务器与外网隔离”的实验目标!


    第一步:打开转发,配置 NAT(解决“怎么去”和“怎么回”的问题)

    1. echo 1 >> /proc/sys/net/ipv4/ip_forward

    • 作用:开启 Linux 系统的路由转发功能。
    • 为什么需要:你的 server1 作为调度器,当外部用户的请求打到它身上时,它不能自己处理,必须把数据包“转发”给后面的 RS 服务器。如果不写这个 1(默认为0),系统内核会直接丢弃这些需要转发的数据包。

    2. iptables -t nat -F

    • 作用:清空 nat 表(Network Address Translation)原有的所有规则。
    • 为什么需要:这是为了排除干扰,防止之前配置的旧 NAT 规则影响当前实验,相当于“清理桌面”。

    3. iptables -t nat -A POSTROUTING -s 192.168.195.0/24 -o ens160 -j MASQUERADE

    • 作用:配置源地址伪装(SNAT),这是 NAT 模式的核心。
    • 详细解释:
      • 当后端的 RS 服务器(在 192.168.195.0/24 网段)处理完请求,准备把网页数据发回给最终用户时,因为它们处于内网,无法直接连通外网。
      • 这条命令的意思是:凡是来自 192.168.195.0/24 网段的数据包,在从 ens160 这块网卡(通常是外网网卡)发出去的时候,把数据包的“发件人 IP”偷偷改成 server1自己的外网 IP。
      • 这样用户收到数据时,以为是server1 发的,从而实现完整的双向通信。

    第二步:设置 wrr 策略和添加 RS 主机(解决“分给谁”的问题)

    4. ipvsadm -A -t 192.168.226.10:80 -s wrr

    • 作用:创建一个对外提供服务的“虚拟 IP”(VIP)。
    • 参数拆解:
      • -A:添加一个虚拟服务 (Add Virtual Service)。
      • -t 192.168.226.10:80:指定对外暴露的 IP和端口(这就是用户最终访问的地址)。
      • -s wrr:指定调度算法为 加权轮询 (Weighted Round Robin)。意思是按权重轮流分配,谁的权重高,谁接到的请求就多。

    5. ipvsadm -a -t 192.168.226.10:80 -r 192.168.195.11:80 -m -w 1 6. ipvsadm -a -t 192.168.226.10:80 -r 192.168.195.12:80 -m -w 1

    • 作用:把真正干活的服务器(RS)加入到这个虚拟服务里。
    • 参数拆解:
      • -a:添加真实服务器 (Add Real Server)。
      • -r 192.168.195.11:80:指定真实服务器的内网IP和端口。
      • -m:指定使用 NAT 模式 (Masquerading)。
      • -w 1:给这台服务器分配的权重是 1。因为两台都是 1,所以请求会 1:1 平分。

    第三步:查看策略信息(验证成果)

    7. ipvsadm -ln

    • 作用:以数字格式(-n)列出(-l)当前的 LVS 规则。
    • 为什么需要:让你检查刚才配置的对不对。

    输出结果

    text

    TCP  192.168.226.10:80 wrr
      -> 192.168.195.11:80       Masq    1      0          0
      -> 192.168.195.12:80       Masq    1      0          0
    • Masq 说明是 NAT 模式。
    • 权重都是 1,且状态正常(后面的连接数为 0)。

    ==清空旧的nat规则防止干扰,开启系统内核的转发功能,配置SNAT源地址伪装实现NAT模式,最后把真实服务器(RS)加入到LVS虚拟服务中==

    image-20260914095728720

    image-20260914100354997


    第一阶段:临时借用 NAT 网络(安装软件)

    1. 关机:先将作为 RS 的虚拟机(server2 或 server3)关机。
      1. 添加网卡:在 VMware 的虚拟机设置里,添加一块新网卡,网络连接选择 “NAT 模式”。此时这台机器有了一块 NAT 网卡和一块仅主机网卡。
    2. 开机联网:启动虚拟机,确保系统能正常获取到 NAT 网卡分配的 IP 并能 ping 通外网(比如 ping www.baidu.com)。
    3. 安装 Nginx:执行安装命令 yum install nginx -y

    image-20260914105442469

    image-20260914111004409


    LVS 的DR案例

    image-20260914114647548

    LVS之DR案例
    # DS部署
    [root@server1 ~]# ifconfig ens160:0 192.168.226.100 broadcast 192.168.226.255 netmask 255.255.255.0 up
    [root@server1 ~]# route add -host 192.168.226.100 dev ens160:0
    [root@server1 ~]# ipvsadm -A -t 192.168.226.100:80 -s wrr
    [root@server1 ~]# ipvsadm -a -t 192.168.226.100:80 -r 192.168.226.20:80 -g -w 2
    [root@server1 ~]# ipvsadm ‐a ‐t 192.168.226.100:80 ‐r 192.168.226.30:80 ‐g ‐w 1
    # RS部署
    [root@server2 ~]# cat rs.sh
    #!/bin/bash
    vip=192.168.226.100
    ifconfig lo:0 $vip broadcast 192.168.226.255 netmask 255.255.255.255 up
    route add -host $vip lo:0
    echo "1" >/proc/sys/net/ipv4/conf/lo/arp_ignore
    echo "2" >/proc/sys/net/ipv4/conf/lo/arp_announce
    echo "1" >/proc/sys/net/ipv4/conf/all/arp_ignore
    echo "2" >/proc/sys/net/ipv4/conf/all/arp_announce
    在所有RS上部署!
    
    不要在DS上直接进行测试,可能会因为网络的问题导致丢包
    

    1. 如果绑在物理网卡上(灾难发生)

    • 现象:RS 的物理网卡上有了 VIP,它就会认为“我是这个 VIP”。
    • ARP 响应:当客户端在局域网里广播:“谁是 192.168.226.100?”时,RS 的物理网卡会立刻抢答:“我是!我的 MAC 地址是 xx:xx…”。
    • 结果:客户端直接把请求发给了 RS。DS(调度器)彻底被架空,负载均衡失效。而且DS 也会抢答,导致局域网里有两个“192.168.226.100”,引发 ARP冲突,网络时通时断,彻底瘫痪。

    2. 绑在回环接口 lo 上(完美方案)

    • 现象:RS 把 VIP 绑在 lo(内部对讲机)上。
    • 对外(装聋作哑):因为 lo不是物理网卡,它接收不到外部局域网发来的ARP广播。RS 假装不知道外面有人在找 VIP。
    • 对内(负责干活):当DS 把请求转发过来时,数据包里写着的目标 IP 是 VIP。RS 拆开包一看:“目标 IP 是 192.168.226.100,我 lo 接口上正好有这个 IP,这包是给我的!” 于是收下并处理。
    • 配合ARP 抑制:为了双重保险,那四行 echo 命令还强制告诉内核:“如果别人问 VIP 的 MAC 地址,你要假装没听见(arp_ignore);如果要主动发ARP 请求,千万别用 VIP 当源 IP(arp_announce)。”

    总结:

    • 物理网卡绑 VIP = 对外广播“我是 VIP”,会引发混乱。
    • lo 绑 VIP = 对外隐身,对内干活,完美承接 DS 转发来的包。

    一、 网络拓扑与核心IP

    • DS(调度器):server1,物理 IP 为 192.168.226.10,网卡名为 ens160。
    • RS(真实服务器):server2(IP: .20)和 server3(IP: .30)。
    • VIP(虚拟IP):192.168.226.100。
      • 特点:DS 和所有 RS 身上都拥有这个 VIP,这是 DR 模式的核心。

    二、 DS(调度器)部署详解原因

    1. ifconfig ens160:0 192.168.226.100 broadcast 192.168.226.255 netmask 255.255.255.0 up

    • 作用:在物理网卡 ens160 上,绑定一个虚拟子接口 ens160:0,并配置 VIP 192.168.226.100。
    • 为什么这么做:这是对外服务的唯一入口。客户端只知道这个 VIP,不知道后面的 RS。配置在子接口上是为了逻辑清晰,方便后续维护。

    2. route add -host 192.168.226.100 dev ens160:0

    • 作用:添加一条主机路由,强制去往 VIP 的流量从 ens160:0 接口出去。
    • 为什么这么做:Linux 系统默认的路由策略可能会把包发到别的网卡上。加这条路由是为了精准锁定,确保 DS能正确处理 VIP 的流量。

    3. ipvsadm -A -t 192.168.226.100:80 -s wrr

    • 作用:创建一个虚拟服务,监听 VIP:80,使用 加权轮询(wrr) 调度算法。
    • 为什么这么做:建立 LVS 的核心账本,告诉系统以后遇到访问 192.168.226.100:80 的请求,要按照权重轮流分配。

    4. ipvsadm -a -t 192.168.226.100:80 -r 192.168.226.20:80 -g -w 2 5. ipvsadm -a -t 192.168.226.100:80 -r 192.168.226.30:80 -g -w 1

    • 作用:把两台 RS 加入集群。
    • 关键参数 -g:这是 DR 模式(Gatewaying) 的标志。它告诉LVS:只修改数据包的 目标 MAC 地址,不要修改 IP,直接把包“扔”给后端真实服务器。
    • 权重 -w:.20 权重是 2,.30 权重是 1。表示每 3 个请求,2 个给 .20,1 个给 .30。

    ==核心总结==:route add -host $vip lo:0 的本质

    1. 本质定义:一个“流量拦截器” 这条路由指令并不是决定数据包最终的物理去向,而是在 Linux 内核的路由表中强行插入了一条规则:凡是目标 IP为 VIP 的数据包,一律不准直接走物理网卡,必须先进入本地回环接口 lo:0。

    2. 核心作用:避免物理网络的“IP 冲突” 为什么要多此一举?因为 RS 在回包时,源 IP 必须是 VIP。如果这个包直接从物理网卡发出去,局域网内的交换机和网关会同时看到 LVS 和 RS 都在使用 VIP,引发严重的 ARP 冲突和网络瘫痪。这条命令把流量关进 lo:0(死胡同),完美避开了物理网络的干扰。

    3. 底层机制:内核的“二次路由”与“偷梁换柱” 数据包进入 lo:0 后,并没有消失。Linux 内核的Netfilter/路由子系统会拦截这个包,发现它其实是发给外部客户端的。于是内核在内部自动完成“二次路由”:

    • 重新查表,找到通往客户端的真正出口(通常是内网网关)。
    • 把数据包重新定向到物理网卡,最终成功发送给网关,且不给局域网添乱。

    4. 最终闭环:LVS-DR 高性能的基石 这条命令与 ifconfig lo:0 $vip 以及 ARP抑制参数配合,让 RS 实现了完美的“隐身收发”:

    • 收得进:靠 VIP 绑在 lo:0 上,接收LVS 转发的请求。
    • 发得出:靠这条主机路由,在内部绕行后从物理网卡直接把响应发给网关。
    • 不冲突:全程不把 VIP 暴露在物理二层网络上,确保了LVS调度器的绝对权威。

    一句话概括: route add -host $vip lo:0 就是给 RS 画了一条“内部单行道”,让 VIP 的流量在本地回环里绕个弯,从而安全、高效地完成响应,彻底解放了 LVS 的带宽压力。

    2.如果没有 route add -host $vip lo:0 这条命令,回包会从哪走?

    是从 RS 自己的物理网卡(比如ens160) 回去。

    模拟一下没有这条路由时,RS1(真实服务器)的尴尬处境:

    1. RS1 收到了 LVS 发来的请求,准备把网页发给客户端。
    2. RS1 构造响应包:源IP 是 VIP(192.168.226.100),目标 IP是客户端(192.168.226.1)。
    3. Linux 系统要发包,会去查本机的路由表:“我要去 192.168.226.1,该从哪个网卡出去?”
    4. 因为 RS1 的物理网卡(ens160)正好在 192.168.226.0/24 网段,系统会默认选择从物理网卡 ens160 发出去。
    5. 灾难发生了:这个包一旦从物理网卡发出去,源IP 就是 192.168.226.100(VIP)。
    6. 局域网里的交换机和网关看到这个包,会瞬间懵圈:“VIP 不是绑在LVS 身上的吗?怎么 RS1 也在用这个IP发包?”。这会导致网络里出现 IP 地址冲突 /ARP 混乱,整个 LVS 集群可能会直接瘫痪断网。

    # 1. 定义 VIP 变量(方便后续调用)
    vip=192.168.226.100
    
    # 2. 在回环接口 lo:0 上绑定 VIP,子网掩码必须是 255.255.255.255
    ifconfig lo:0 $vip broadcast 192.168.226.255 netmask 255.255.255.255 up
    
    # 3. 添加一条去往 VIP 的主机路由,确保响应流量走 lo:0 返回
    route add -host $vip lo:0
    
    # 4. 配置 ARP 抑制参数(防止 RS 抢 LVS 的 VIP),分两行写入
    echo "1" > /proc/sys/net/ipv4/conf/lo/arp_ignore
    echo "2" > /proc/sys/net/ipv4/conf/lo/arp_announce
    echo "1" > /proc/sys/net/ipv4/conf/all/arp_ignore
    echo "2" > /proc/sys/net/ipv4/conf/all/arp_announce

    image-20260914181647186

    image-20260914182007062


    三、 从“外网/内网”角度的最终对比总结

    作为客户端,从网络架构看,区别天壤之别:

    对比维度 NAT 模式 DR 模式
    客户端看到的源IP 永远是 VIP 永远是 VIP
    LVS 在回包时的角色 绝对主力(必须经手回包并改源 IP) 隐身人(响应包直接由 RS 发给网关)
    网络结构要求 LVS和 RS 可以不在同一网段 LVS和 RS 必须在同一网段(同一个二层网络)
    外网到内网的流向 客户端->LVS->RS->LVS->客户端 客户端->LVS->RS->网关->客户端
    谁负责流量瓶颈 LVS(压力大,因为要处理所有进出流量) RS 和网关(LVS只处理很小的请求包)
    生产环境适用场景 适合并发量不太高的场景,或者内网服务器需要连接外网 互联网大厂主流,适合高并发、大流量的负载均衡

    一句话帮您彻底记住:

    • NAT 模式:LVS是全包圆的苛刻包工头,所有活儿(进和出)都要经过它。

    Keepalived+LVS+httpd实现网站负载均衡高可用

    这套架构的目的是:让用户通过访问一个虚拟的IP(VIP),自动把请求分摊到两台真实服务器上;如果主调度器挂了,备用调度器会自动顶上,保证服务不中断。

    # 设备详细:
    server1(DS1):192.168.226.10
    server2(DS2):192.168.226.20
    server3(RS1):192.168.226.30
    server4(RS2):192.168.226.40
    VIP:192.168.226.100(单主模型)
    
    #在server3和server4执行下面的脚本:
    #!/bin/bash
    yum install httpd net-tools -y
    systemctl stop firewalld
    setenforce 0
    vip="192.168.226.100"
    mask="255.255.255.255"
    ifconfig lo:0 $vip broadcast $vip netmask $mask up
    route add -host $vip lo:0
    echo "1" >/proc/sys/net/ipv4/conf/lo/arp_ignore
    echo "2" >/proc/sys/net/ipv4/conf/lo/arp_announce
    echo "1" >/proc/sys/net/ipv4/conf/all/arp_ignore
    echo "2" >/proc/sys/net/ipv4/conf/all/arp_announce
    echo "<h1>This is RS1</h1>" > /var/www/html/index.html # 修改不同的主页以便测试!
    systemctl start httpd
    
    # 在server1和server2上配置keepalived(包含LVS的配置)
    [root@server2 ~]# vim /etc/keepalived/keepalived.conf   # 最好事先备份一下mv /etc/keepalived/keepalived.conf  /etc/keepalived/keepalived.conf.bak
    
    ! Configuration File for keepalived
    global_defs {
        router_id server1 # 设置lvs的id,一个网络中应该唯一
    }
    vrrp_instance VI_1 {
        state MASTER # 指定Keepalived的角色
        interface ens160 # 网卡
        virtual_router_id 10 # 虚拟路由器ID,主备需要一样
        priority 150 # 优先级越大越优,backup路由器需要设置比这小!
        advert_int 1 # 检查间隔1s
        authentication {
        auth_type PASS
        auth_pass 1111
        }
        virtual_ipaddress {
        192.168.226.100 # 定义虚拟IP地址,可以定义多个
        }
    }
    # 定义虚拟主机,对外服务的IP和port
    virtual_server 192.168.226.100 80 {
        delay_loop 6 # 设置健康检查时间,单位是秒
        lb_algo wrr # 负责调度算法
        lb_kind DR # LVS负载均衡机制
        persistence_timeout 0
        protocol TCP
    # 指定RS主机IP和port
        real_server 192.168.226.30 80 {
        weight 2
    # 定义TCP健康检查
        TCP_CHECK {
        connect_timeout 10
        nb_get_retry 3
        delay_before_retry 3
        connect_port 80
        }
    }
    real_server 192.168.226.40 80 {
        weight 1
        TCP_CHECK {
        connect_timeout 10
        nb_get_retry 3
        delay_before_retry 3
        connect_port 80
        }
        }
    }
    

    第一部分:全局定义 (global_defs)

    nginx

    global_defs {
        router_id server1 
    }
    • 作用:定义这台Keepalived 节点的身份标识。
    • router_id server1:给这台机器起个名字。在同一个局域网的高可用集群中,每台机器的 router_id 必须唯一。
      • 注意:你在 server2 上操作,如果 server2 是备机,改成 router_id server2 以防混淆

    第二部分:VRRP 实例配置 (vrrp_instance VI_1)

    这是Keepalived 实现故障自动切换(高可用)的核心部分。

    nginx

    vrrp_instance VI_1 {
        state MASTER 
        interface ens160 
        virtual_router_id 10 
        priority 150 
        advert_int 1 
        authentication { ... }
        virtual_ipaddress { 192.168.226.100 }
    }
    • state MASTER:定义这台机器的初始角色是主节点。另一台备机(server2)这里必须写 state BACKUP。
    • interface ens160:指定虚拟IP(VIP)绑定在哪块物理网卡上。我的网卡名是 ens160。
    • virtual_router_id 10:虚拟路由组 ID。主备两台机器这个数字必须完全一致(都是 10),它们才能互相识别为同一组。
    • priority 150:优先级。数字越大,越容易成为主节点(Master)。备机的优先级必须小于 150(比如设成 100)。当 Master 宕机不再发送心跳时,Backup 会根据优先级自动抢占成为新的 Master。
    • advert_int 1:心跳广播间隔,1 秒一次。Master 每隔 1 秒告诉 Backup “我还活着”。
    • authentication:主备通信的密码认证(密码是 1111),主备必须一致。
    • virtual_ipaddress { 192.168.226.100 }:最关键的配置。这就是对外提供服务的虚拟IP(VIP)。正常情况下,这个IP 绑定在 Master 机器上;Master 宕机后,这个IP 会瞬间“漂移”到 Backup 机器上。

    第三部分:LVS 虚拟服务器与真实服务器配置 (virtual_server)

    这部分是LVS 负载均衡 的核心。它告诉系统如何分发到达 VIP 的流量。

    nginx

    virtual_server 192.168.226.100 80 {
        delay_loop 6 
        lb_algo wrr 
        lb_kind DR 
        persistence_timeout 0
        protocol TCP
    
    • virtual_server 192.168.226.100 80:定义了一个虚拟服务,监听 VIP 的 80 端口。
    • delay_loop 6:健康检查间隔,每 6 秒检查一次后端服务器是否存活。
    • lb_algo wrr:加权轮询(Weighted Round Robin)算法。意思是根据服务器的性能分配请求,性能好的(权重高)多分点请求。
    • lb_kind DR:直接路由(Direct Routing)模式。这是 LVS性能最高的模式(请求发给 LVS,LVS 只改 MAC 地址丢给后端服务器,后端服务器直接把结果返回给客户端,不再经过LVS)。
    • persistence_timeout 0:会话保持时间。设为 0 表示不保持(同一个用户的请求可能分发给不同的后端服务器)。如果需要保持登录状态,可以设为 600(10分钟)。
    • protocol TCP:指定服务协议。

    真实服务器定义 (real_server)

    在 virtual_server 内部,定义了两台后端真实提供 Web 服务的机器:

    nginx

        real_server 192.168.226.30 80 {
            weight 2
            TCP_CHECK {
                connect_timeout 10
                nb_get_retry 3
                delay_before_retry 3
                connect_port 80
            }
        }
        real_server 192.168.226.40 80 {
            weight 1
            TCP_CHECK { ... }
        }
    • real_server 192.168.226.30 80:第一台真实服务器的IP 和端口。
    • weight 2:权重为 2。
    • real_server 192.168.226.40 80:第二台真实服务器的 IP和端口。
    • weight 1:权重为 1。
    • 分配比例:因为权重是2:1,所以 LVS会把 2/3 的请求发给 .30,1/3 的请求发给 .40。
    • TCP_CHECK:健康检查机制。LVS会每隔 6 秒(delay_loop)去尝试连接这两台机器的 80 端口。如果连不上(超时 10 秒,重试 3 次),LVS 就会自动把这台挂掉的服务器从列表中剔除,不再往它上面发请求,直到它恢复正常。

    核心警告:LVS-DR 模式的“隐藏要求”

    配置里用了 lb_kind DR。配置完这个文件,结果发现访问 VIP 根本不通。因为 DR 模式必须在后端真实服务器(192.168.226.30 和 .40)上也做特殊配置:

    1. 抑制 ARP 广播:必须在后端服务器的网卡上关闭 ARP广播(修改内核参数 arp_ignore 和 arp_announce)。否则,客户端寻找 VIP(.100)时,后端服务器会回应,导致客户端直接连到了后端服务器,绕过了调度器(这就失去了负载均衡的意义)。
    2. 绑定 VIP 到本地回环口:后端服务器需要在 lo:0 上绑定 VIP 192.168.226.100,这样它才能接收目标地址是 VIP 的数据包。

    总结与避坑指南

    1. 主备配置差异:在 server2 上配置时,必须修改:state BACKUP、priority 100(比 150 小)、router_id server2。其他的保持不变。
    2. 多台机器准备:
      • 调度器(LVS):192.168.226.20 和 192.168.226.10 (主备)
      • 真实服务器(RS):192.168.226.30 和 192.168.226.40(需要安装并启动 Nginx/Apache,且配置好 DR 模式)
      • 虚拟IP(VIP):192.168.226.100
    3. 启动与测试:配置完成后,重启Keepalived 服务(systemctl restart keepalived)。此时在调度器上执行 ip a,你应该能看到 ens160 上多出了一个 192.168.226.100 的IP。然后用浏览器访问 http://192.168.226.100,测试负载均衡和高可用是否生效。

    实践操作

    📁 .bak 文件作用标准总结

    1. 核心定义 .bak 是英文 backup 的缩写,意为“备份”。在 Linux 系统中,它通常作为文件的后缀名(如 keepalived.conf.bak),用来标识这是一个配置文件的副本。

    2. 标准操作规范(最佳实践)

    • 改前必做:修改任何系统或服务的关键配置文件(如 nginx.conf、keepalived.conf、/etc/fstab 等)之前,必须先备份。

    • mv file file.bak:原文件被移走,需要手动新建空文件。若忘记新建,服务会直接报错

      1. 日志与 VIP 生效(最关键的部分):
        • 看日志区域,有几行非常关键的提示:
          • Sending gratuitous ARP on ens160 for 192.168.226.100:Keepalived 正在向局域网发送广播(ARP),告诉所有设备:“虚拟IP 192.168.226.100 现在在我这台机器上!” 这样客户端访问 .100 时,流量才会导过来。
          • TCP connection to [192.168.226.40]:tcp:80 success 和 [192.168.226.30]:tcp:80 success:LVS 健康检查成功! 这说明你的后端真实服务器(RS: .30 和 .40)的 80 端口是通的,LVS 已经成功识别到了这两台服务器。
      2. IP 验证:
        • 最后执行 ip a,你可以看到 ens160 网卡上不仅有原本的 IP 192.168.226.10,还多出了 VIP 192.168.226.100。这证明主机已经成功接管了虚拟IP。

      总结:第一张图代表主节点已经完美上线,正在对外提供服务,并且成功接管了虚拟 IP

    • BACKUP节点
    1. 日志解读:
      • 备机的日志通常没有主机的“Sending gratuitous ARP”那么活跃,因为它不需要发广播宣告主权。它此时正在默默接收主机发来的心跳包(VRRP广播)。

    总结:第二张图代表备节点已经处于待命状态,配置正确,随时准备在主机故障时接管业务。

    image-20260917125315500

    image-20260917125650826

    image-20260917130328745

    image-20260917130529161

    image-20260917130647559


    HAProxy介绍

    HAProxy 是一款广泛使用的高性能开源软件,主要用于提供高可用性、负载均衡以及基于 TCP 和 HTTP 的应用代理。

    解决lua环境

    HAProxy 支持基于lua实现功能扩展,lua是一种小巧的脚本语言,于1993年由巴西里约热内卢天主教大学(Pontifical Catholic University of Rio de Janeiro)里的一个研究小组开发,其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

    Lua 官网:www.lua.org

    • Lua 应用场景

      • 游戏开发
      • 独立应用脚本
      • Web 应用脚本
      • 扩展和数据库插件,如MySQL Proxy
      • 安全系统,如入侵检测系统
    • 安装基础命令及编译依赖环境

    [root@localhost ~]# yum -y install gcc readline-devel
    [root@localhost ~]# wget http://www.lua.org/ftp/lua-5.3.5.tar.gz
    [root@localhost ~]# tar xvf  lua-5.3.5.tar.gz -C /usr/local/src
    [root@localhost ~]# cd /usr/local/src/lua-5.3.5
    [root@localhost lua-5.3.5]# make linux test

    编译安装HAProxy

    • HAProxy2.0以上版本编译参数
    [root@localhost ~]# yum -y install gcc openssl-devel pcre-devel systemd-devel
    [root@localhost ~]# wget http://www.haproxy.org/download/2.1/src/haproxy-2.1.3.tar.gz
    [root@localhost ~]# tar xvf haproxy-2.1.3.tar.gz  -C /usr/local/src
    [root@localhost ~]# cd /usr/local/src/haproxy-2.1.3/
    [root@localhost haproxy-2.1.3]# cat README
    • 参考INSTALL文件进行编译安装
    [root@localhost haproxy-2.1.3]# make -j 2 ARCH=x86_64 TARGET=linux-glibc  
    USE_PCRE=1 
    USE_OPENSSL=1 
    USE_ZLIB=1  
    USE_SYSTEMD=1  
    USE_LUA=1 
    LUA_INC=/usr/local/src/lua-5.3.5/src/  
    LUA_LIB=/usr/local/src/lua-5.3.5/src/
    [root@localhost haproxy-2.1.3]# make install PREFIX=/apps/haproxy
    [root@localhost haproxy-2.1.3]# ln -s /apps/haproxy/sbin/haproxy /usr/sbin/
    • 查看生成的文件
    [root@localhost ~]# tree -C /apps/haproxy/
    /apps/haproxy/
    ├── doc
    │   └── haproxy
    │       ├── 51Degrees-device-detection.txt
    │       ├── architecture.txt
    │       ├── close-options.txt
    │       ├── configuration.txt
    │       ├── cookie-options.txt
    │       ├── DeviceAtlas-device-detection.txt
    │       ├── intro.txt
    │       ├── linux-syn-cookies.txt
    │       ├── lua.txt
    │       ├── management.txt
    │       ├── netscaler-client-ip-insertion-protocol.txt
    │       ├── network-namespaces.txt
    │       ├── peers.txt
    │       ├── peers-v2.0.txt
    │       ├── proxy-protocol.txt
    │       ├── regression-testing.txt
    │       ├── seamless_reload.txt
    │       ├── SOCKS4.protocol.txt
    │       ├── SPOE.txt
    │       └── WURFL-device-detection.txt
    ├── sbin
    │   └── haproxy
    └── share
        └── man
            └── man1
                └── haproxy.1
    
    6 directories, 22 files

    验证HAProxy版本

    [root@localhost ~]# which haproxy
    /usr/sbin/haproxy
    [root@localhost ~]# haproxy -v
    HA-Proxy version 2.1.3 2020/02/12 - https://haproxy.org/
    Status: stable branch - will stop receiving fixes around Q1 2021.
    Known bugs: http://www.haproxy.org/bugs/bugs-2.1.3.html
    
    [root@localhost ~]# haproxy -vv
    HA-Proxy version 2.1.3 2020/02/12 - https://haproxy.org/
    Status: stable branch - will stop receiving fixes around Q1 2021.
    Known bugs: http://www.haproxy.org/bugs/bugs-2.1.3.html
    Build options :
      TARGET  = linux-glibc
      CPU     = generic
      CC      = gcc
      CFLAGS  = -m64 -march=x86-64 -O2 -g -fno-strict-aliasing -Wdeclaration-after-statement -fwrapv -Wno-unused-label -Wno-sign-compare -Wno-unused-parameter -Wno-old-style-declaration -Wno-ignored-qualifiers -Wno-clobbered -Wno-missing-field-initializers -Wtype-limits
      OPTIONS = USE_PCRE=1 USE_OPENSSL=1 USE_LUA=1 USE_ZLIB=1 USE_SYSTEMD=1
    
    Feature list : +EPOLL -KQUEUE -MY_EPOLL -MY_SPLICE +NETFILTER +PCRE -PCRE_JIT -PCRE2 -PCRE2_JIT +POLL -PRIVATE_CACHE +THREAD -PTHREAD_PSHARED -REGPARM -STATIC_PCRE -STATIC_PCRE2 +TPROXY +LINUX_TPROXY +LINUX_SPLICE +LIBCRYPT +CRYPT_H -VSYSCALL +GETADDRINFO +OPENSSL +LUA +FUTEX +ACCEPT4 -MY_ACCEPT4 +ZLIB -SLZ +CPU_AFFINITY +TFO +NS +DL +RT -DEVICEATLAS -51DEGREES -WURFL +SYSTEMD -OBSOLETE_LINKER +PRCTL +THREAD_DUMP -EVPORTS
    
    Default settings :
      bufsize = 16384, maxrewrite = 1024, maxpollevents = 200
    
    Built with multi-threading support (MAX_THREADS=64, default=4).
    Built with OpenSSL version : OpenSSL 1.0.2k-fips  26 Jan 2017
    Running on OpenSSL version : OpenSSL 1.0.2k-fips  26 Jan 2017
    OpenSSL library supports TLS extensions : yes
    OpenSSL library supports SNI : yes
    OpenSSL library supports : SSLv3 TLSv1.0 TLSv1.1 TLSv1.2
    Built with Lua version : Lua 5.3.5
    Built with network namespace support.
    Built with transparent proxy support using: IP_TRANSPARENT IPV6_TRANSPARENT IP_FREEBIND
    Built with PCRE version : 8.32 2012-11-30
    Running on PCRE version : 8.32 2012-11-30
    PCRE library supports JIT : no (USE_PCRE_JIT not set)
    Encrypted password support via crypt(3): yes
    Built with zlib version : 1.2.7
    Running on zlib version : 1.2.7
    Compression algorithms supported : identity("identity"), deflate("deflate"), raw-deflate("deflate"), gzip("gzip")
    
    Available polling systems :
          epoll : pref=300,  test result OK
           poll : pref=200,  test result OK
         select : pref=150,  test result OK
    Total: 3 (3 usable), will use epoll.
    
    Available multiplexer protocols :
    (protocols marked as <default> cannot be specified using 'proto' keyword)
                  h2 : mode=HTTP       side=FE|BE     mux=H2
                fcgi : mode=HTTP       side=BE        mux=FCGI
           <default> : mode=HTTP       side=FE|BE     mux=H1
           <default> : mode=TCP        side=FE|BE     mux=PASS
    
    Available services : none
    
    Available filters :
            [SPOE] spoe
            [CACHE] cache
            [FCGI] fcgi-app
            [TRACE] trace
            [COMP] compression

    HAProxy启动脚本

    [root@localhost ~]# vim /usr/lib/systemd/system/haproxy.service
    [Unit]
    Description=HAProxy Load Balancer
    After=syslog.target network.target
    
    [Service]
    ExecStartPre=/usr/sbin/haproxy -f /etc/haproxy/haproxy.cfg  -c -q
    ExecStart=/usr/sbin/haproxy -Ws -f /etc/haproxy/haproxy.cfg -p /var/lib/haproxy/haproxy.pid
    ExecReload=/bin/kill -USR2 $MAINPID
    
    [Install]
    WantedBy=multi-user.target
    • 默认缺少配置文件,无法启动
    [root@localhost ~]# systemctl daemon-reload
    [root@localhost ~]# systemctl start haproxy
    Job for haproxy.service failed because the control process exited with error code. See "systemctl status haproxy.service" and "journalctl -xe" for details.
    
    
    [root@localhost ~]# tail /var/log/messages
    Jul 13 10:39:33 localhost systemd: Starting HAProxy Load Balancer...
    Jul 13 10:39:33 localhost haproxy: [ALERT] 193/103933 (8156) : Cannot open configuration file/directory /etc/haproxy/haproxy.cfg : No such file or directory
    Jul 13 10:39:33 localhost systemd: haproxy.service: control process exited, code=exited status=1
    Jul 13 10:39:33 localhost systemd: Failed to start HAProxy Load Balancer.
    Jul 13 10:39:33 localhost systemd: Unit haproxy.service entered failed state.
    Jul 13 10:39:33 localhost systemd: haproxy.service failed.

    配置文件

    • 查看配置文件范例
    [root@localhost ~]# tree -C /usr/local/src/haproxy-2.1.3/examples/
    /usr/local/src/haproxy-2.1.3/examples/
    ├── acl-content-sw.cfg
    ├── content-sw-sample.cfg
    ├── errorfiles
    │   ├── 400.http
    │   ├── 403.http
    │   ├── 408.http
    │   ├── 500.http
    │   ├── 502.http
    │   ├── 503.http
    │   ├── 504.http
    │   └── README
    ├── haproxy.init
    ├── option-http_proxy.cfg
    ├── socks4.cfg
    ├── transparent_proxy.cfg
    └── wurfl-example.cfg
    
    1 directory, 15 files
    • 创建自定义的配置文件
    [root@localhost ~]# mkdir  /etc/haproxy
    [root@localhost ~]# vim /etc/haproxy/haproxy.cfg
    global
        maxconn 100000
        chroot /apps/haproxy
        stats socket /var/lib/haproxy/haproxy.sock mode 600 level admin
        #uid 99
        #gid 99
        user  haproxy
        group haproxy
        daemon
        #nbproc 4
        #cpu-map 1 0
        #cpu-map 2 1
        #cpu-map 3 2
        #cpu-map 4 3
        pidfile /var/lib/haproxy/haproxy.pid
        log 127.0.0.1 local2 info
    
    defaults
        option http-keep-alive
        option  forwardfor
        maxconn 100000
        mode http
        timeout connect 300000ms
        timeout client  300000ms
        timeout server  300000ms
    
    listen stats
        mode http
        bind 0.0.0.0:9999
        stats enable
        log global
        stats uri     /haproxy-status
        stats auth    admin:123456
    
    listen  web_port
        bind 192.168.226.10:80
        mode http
        log global
        server web1  127.0.0.1:8080  check inter 3000 fall 2 rise 5

    启动haproxy

    [root@localhost ~]# mkdir  /var/lib/haproxy
    [root@localhost ~]# useradd -r -s /sbin/nologin -d /var/lib/haproxy haproxy
    [root@localhost ~]# systemctl  enable --now haproxy

    编译安装HAProxy

    [root@server1 haproxy-2.1.3]# make -j 2 ARCH=x86_64 TARGET=linux-glibc  USE_PCRE=1  USE_SYSTEMD=1  USE_LUA=1 LUA_INC=/usr/local/src/lua-5.3.5/src/  LUA_LIB=/usr/local/src/lua-5.3.5/src/
    
    HAProxy中的bind用法
    1. 监听所有 IP 的指定端口
    frontend http_front
        bind *:80         # 监听所有 IPv4 地址的 80 端口
        bind :::80        # 监听所有 IPv6 地址的 80 端口(双栈支持)
        bind *:443 ssl    # 监听 443 端口并启用 SSL(需配合证书配置)
    2. 监听特定 IP 地址
    frontend web_front
        bind 192.168.1.100:80   # 仅通过 192.168.1.100 接收 80 端口请求
        bind 203.0.113.5:443    # 仅通过公网 IP 203.0.113.5 接收 HTTPS 请求
    3. 监听端口范围
        frontend app_front
        bind *:8000-8010    # 监听 8000 到 8010 之间的所有端口
    4. 配置 SSL/TLS 加密(HTTPS)
    frontend https_front
        bind *:443 ssl crt /etc/haproxy/certs/example.com.pem
        # 说明:
        # - ssl:启用 SSL 加密
        # - crt:指定 PEM 格式的证书文件(包含证书和私钥,需合并为一个文件)
    5. 限制协议类型
    frontend tcp_front
        bind *:8080 proto tcp  # 仅接收 TCP 协议(默认)
        mode tcp               # 配合 mode 指令使用
    
    frontend udp_front
        bind *:53 proto udp    # 接收 UDP 协议(如 DNS 服务)
        mode udp
    6. 配置监听队列与连接限制
    frontend high_traffic_front
        bind *:80 backlog 10000 maxconn 5000
        # 监听队列大小 10000,最大并发连接 5000
    7. 绑定到特定网络接口
    frontend lan_front
        bind eth0:80   # 仅通过局域网网卡 eth0 接收 80 端口请求
    8. 结合 listen 块使用
    listen web_service
        bind *:80
        mode http
        server web1 192.168.1.101:80 check
        server web2 192.168.1.102:80 check
    
    

    HAProxy的统计报告页面(Stats Page)

    image-20260922103508734

    简单来说,它的作用和意义如下:

    • 是什么:它是 HAProxy 自带的一个内置可视化网页,用来展示当前负载均衡器的实时运行数据和健康状态。
    • 干啥的:用来监控和排错。通过它,你可以直观地看到有哪些前端(Frontend)在接收流量、后端(Backend)有哪些服务器、这些服务器当前是正常的(绿色 UP)还是故障的(红色 DOWN)、连接数有多少、流量有多大等。

    HAProxy的配置文件内容

    文件路径为 /etc/haproxy/haproxy.cfg

    HAProxy是一款广泛使用的高性能开源软件,主要用于提供高可用性、负载均衡以及基于 TCP 和 HTTP 的应用代理。

    1. global 段(全局配置)

    这一部分主要设置 HAProxy 进程级别的参数,影响整个服务的运行。

    • maxconn 100000: 设定每个 HAProxy 进程可接受的最大并发连接数为 100,000。
    • chroot /apps/haproxy: 将 HAProxy 的工作目录切换到 /apps/haproxy。这是一种安全措施(沙盒机制),即使 HAProxy 被攻破,攻击者也只能访问该目录下的文件。
    • stats socket /var/lib/haproxy/haproxy.sock mode 600 level admin: 定义一个 Unix Socket 用于管理。权限设为 600(仅所有者读写),权限级别为 admin(允许执行所有管理命令)。
    • #uid 99 和 #gid 99:这两行被注释掉了(前面有 #)。原本用于指定运行 HAProxy 的用户 ID 和组 ID。
    • user haproxy 和 group haproxy: 指定 HAProxy 进程运行的用户和组为 haproxy。出于安全考虑,不应该使用 root 用户运行。
    • daemon: 让 HAProxy 以守护进程(后台)模式运行。
    • #nbproc 4 及随后的 #cpu-map ...: 这些都被注释掉了。原本用于设置启动 4 个 HAProxy 进程,并将它们绑定到不同的 CPU 核心上(多核优化)。
    • pidfile /var/lib/haproxy/haproxy.pid: 指定存放 HAProxy 进程 ID (PID) 的文件路径。
    • log 127.0.0.1 local2 info: 配置日志输出。将日志发送到本地回环地址(127.0.0.1)的 local2 设施(Facility),日志级别为 info。

    2. defaults 段(默认配置)

    这一部分为后续的 listen、frontend 和 backend 段提供默认参数。

    • option http-keep-alive: 开启 HTTP 长连接(Keep-Alive),允许在同一个 TCP 连接上发送多个 HTTP 请求,减少延迟。
    • option forwardfor: 如果后端服务器需要记录客户端的真实 IP 地址,HAProxy 会在 HTTP 请求头中插入 X-Forwarded-For 字段。
    • maxconn 100000: 设定每个前端(frontend)或监听器(listen)的最大并发连接数。
    • mode http: 设置默认的工作模式为 HTTP(七层代理)。其他模式还有 tcp(四层代理)。
    • timeout connect 300000ms: 连接后端服务器的超时时间设为 300,000 毫秒(即 5 分钟)。
    • timeout client 300000ms: 客户端连接的空闲超时时间设为 300,000 毫秒。
    • timeout server 300000ms: 后端服务器连接的空闲超时时间设为 300,000 毫秒。

    3. listen stats 段(统计监控配置)

    这一部分定义了一个用于查看 HAProxy 运行状态和统计信息的 Web 页面。

    • mode http: 该监听器工作在 HTTP 模式。
    • bind 0.0.0.0:9999: 监听所有网卡的 9999 端口。
    • stats enable: 启用统计报告功能。
    • log global: 使用全局配置中定义的日志设置。
    • stats uri /haproxy-status: 设置访问统计页面的 URL 路径为 /haproxy-status(例如:http://<IP>:9999/haproxy-status)。
    • stats auth admin:123456: 设置访问统计页面的认证信息,用户名为 admin,密码为 123456。

    4. listen web_port 段(业务负载均衡配置)

    这一部分定义了一个具体的负载均衡监听器,用于转发 Web 流量。

    • bind 192.168.226.10:80: 监听本机的 192.168.226.10 地址的 80 端口(HTTP 标准端口)。
    • mode http: 工作在 HTTP 模式。
    • log global: 使用全局日志设置。
    • server web1 127.0.0.1:8080 check inter 3000 fall 2 rise 5: 定义后端服务器。
      • 名称:web1
      • 地址:127.0.0.1:8080(本地回环地址的 8080 端口)。
      • check: 开启健康检查。
      • inter 3000: 每 3000 毫秒(3秒)检查一次。
      • fall 2: 如果连续 2 次检查失败,则认为该服务器不可用(下线)。
      • rise 5: 如果连续 5 次检查成功,则认为该服务器恢复正常(上线)。

    总结

    这个配置文件的逻辑是:

    1. 全局:设置基本的安全、日志和性能参数。
    2. 默认:设定通用的 HTTP 超时和代理选项。
    3. Stats:在 9999 端口提供一个带密码保护的监控页面。
      1. 业务:在 192.168.226.10 的 80 端口对外提供服务,将流量转发给本地 8080 端口上的一个 Web 服务(web1),并带有健康检查机制。

    日志配置

    HAProxy配置

    # 在global配置项定义
    log 127.0.0.1  local{1-7} info #基于syslog记录日志到指定设备,级别有(err、warning、info、debug)
    
    listen  web_port
      bind 127.0.0.1:80
      mode http
      log global                                #开启当前web_port的日志功能,默认不记录日志
      server web1  127.0.0.1:8080  check inter 3000 fall 2 rise 5
    
    # systemctl  restart haproxy

    Rsyslog配置

    [root@server1 ~]# vim /etc/rsyslog.conf
    $ModLoad imudp
    $UDPServerRun 514
    
    $ModLoad imtcp
    $InputTCPServerRun 514
    ..........
    local3.*    /var/log/haproxy.log
    
    [root@server1 ~]# systemctl restart rsyslog.service
    [root@server1 ~]# vim /etc/haproxy/haproxy.cfg
    global
        log 127.0.0.1 local3 info
    
    listen web_port_81
        log global
    [root@server1 ~]# systemctl restart haproxy.service
    [root@server1 ~]# tail -f /var/log/haproxy.log

    将 HAProxy 的日志通过 Rsyslog 记录到本地文件中。简单来说:HAProxy配置中的 local3 是“发送方”的标签,而Rsyslog 中的两行是“接收方”的规则。 它们必须配合使用才能让日志正常落盘。这台服务器同时通过 UDP 和TCP 协议在 514 端口接收外部发来的日志

    1. HAProxy 配置文件中的 log 127.0.0.1 local3 info

    在 global 段中,你修改了日志配置:

    haproxy

    log 127.0.0.1 local3 info
    • 127.0.0.1: HAProxy本身不直接将日志写入硬盘文件(为了提高性能和避免 I/O 阻塞),而是将日志发送给系统的日志服务(Rsyslog)。这里指定发送到本机的Rsyslog服务。
    • local3: 这是 Syslog 的设施(Facility)。Linux 系统将日志来源分类为不同的“设施”,如 auth(认证)、cron(定时任务)、mail(邮件)等。local0 到 local7 是预留给用户自定义程序使用的。这里 HAProxy 选择了 local3 作为自己的专属标签。
    • info: 这是日志级别(Level)。表示记录 info 级别及更高级别(如 warning, error 等)的日志。
    • 意义:告诉HAProxy:“把你产生的日志,打上 local3 的标签,发送给本地的 Rsyslog 服务

    2. Rsyslog 配置文件中的两行

    在 /etc/rsyslog.conf 文件的 #### RULES #### 部分下方(大约第 68-69 行),你增加了:

    rsyslog

    local3.*                                                /var/log/haproxy.log
    local3.*                                                ~
    • local3.*: 这里的 * 代表所有级别(包括 info, warning, error 等)。意思是匹配所有带有 local3 标签的日志消息。

    • /var/log/haproxy.log: 动作指令。意思是:“将所有匹配到的日志,写入到 /var/log/haproxy.log 这个文件中”。

    • local3.* ~: 这里的波浪号 ~ 是 Rsyslog 中的一个特殊动作,代表 丢弃(Discard)。

    • 意义:

      • 第一行定义了日志的存储位置。
      • 第二行非常关键:因为 Rsyslog 的规则是从上往下匹配的,如果不加 ~,这些 local3 的日志不仅会写入 /var/log/haproxy.log,还会继续向下匹配,最终被写入系统的 /var/log/messages 中。加上 ~ 就是为了防止日志重复记录,告诉 Rsyslog:“把这些 local3 的日志存到指定文件后,就不要再往下传递处理了。”

    3. ModLoad imudp (加载 UDP 模块)

    • ModLoad: 这是 Rsyslog 的指令,意思是“加载模块”(Module Load)。
    • imudp: 代表 Input Module UDP(UDP 输入模块)。
    • 意义:Rsyslog 默认情况下只接收本机系统产生的日志(通过 Unix Socket 或本地机制)。==如果你想让它接收通过网络传过来的日志(比如另一台服务器发来的),你必须加载相应的网络接收模块==。这行代码就是告诉Rsyslog:“准备好接收UDP 协议的网络日志”。

    4. UDPServerRun 514 (在 514 端口运行UDP 服务)

    • UDPServerRun: 这是配合 imudp 模块使用的指令,意思是“启动UDP 服务器并监听”。
    • 514: 这是 Syslog 协议的标准端口号。
    • 意义: 告诉 Rsyslog:“在 514 端口上监听,等待其他设备发送 UDP 日志过来”。

    因为写的是 127.0.0.1(本机),所以 HAProxy 是通过本地内部机制把日志交给Rsyslog 的,并不需要走网络UDP。

    image-20260922121400037

    总结

    这是一个典型的 “谁产生,谁打标;谁接收,谁分流” 的流程:

    1. HAProxy产生日志,并贴上 local3 的标签发送给 Rsyslog。
    2. Rsyslog 接收日志,看到标签是 local3。
    3. 根据 Rsyslog的规则,Rsyslog 把带有 local3 的日志提取出来,写入到 /var/log/haproxy.log 文件中,并阻止它们混入系统的常规日志里。

    image-20260922141839181

    image-20260922142542334

    因为 ModLoad 和 UDPServerRun 是两个完全不同的指令,它们之间必须用换行(或者至少是空格)分隔开,否则系统会把它们当成一个整体,导致识别失败。

    image-20260922143115710


    frontend+backend配置实例

    image-20260922144001472

    haproxy配置文件

    [root@haproxy ~]# vim /etc/haproxy/haproxy.cfg
    global
        maxconn 100000
        chroot /apps/haproxy
        stats socket /var/lib/haproxy/haproxy.sock mode 600 level admin
        #uid 99
        #gid 99
        user  haproxy
        group haproxy
        daemon
        #nbproc 4
        #cpu-map 1 0
        #cpu-map 2 1
        #cpu-map 3 2
        #cpu-map 4 3
        pidfile /var/lib/haproxy/haproxy.pid
        log 127.0.0.1 local3 info
    
    defaults
        option http-keep-alive
        option  forwardfor
        maxconn 100000
        mode http
        timeout connect 300000ms
        timeout client  300000ms
        timeout server  300000ms
    
    listen stats
        mode http
        bind 0.0.0.0:9999
        stats enable
        log global
        stats uri     /haproxy-status
        stats auth    admin:123456
    
    frontend test-http
        bind *:80
        mode tcp
        use_backend test-http-nodes
    
    backend test-http-nodes
        mode tcp
        default-server inter 1000 weight 6
        server web1 192.168.226.20:80 check inter 3000 fall 3 rise 5 weight 2 addr 192.168.226.20 port 3306
        server web2 192.168.226.30:80 check inter 3000 fall 3 rise 5
    
    [root@haproxy ~]# systemctl restart haproxy

    image-20260922154701166

    一、 Frontend(前端模块):负责“接客”

    代码位置: 第 41 – 44 行

    haproxy

    frontend test-http
        bind *:80
        mode tcp
        use_backend test-http-nodes

    前端模块的作用是定义 HAProxy 对外的入口,即监听哪个端口、接收什么流量,并把流量转发给谁。

    • frontend test-http:
      • 定义了一个名为 test-http 的前端。名字是自定义的,方便管理。
    • bind *:80:
      • 让 HAProxy 监听本机所有网卡(*)的 80 端口。任何发往这台机器 80 端口的请求都会被这个前端捕获。
    • mode tcp:
      • 重点注意:这里设置的是 TCP 模式(四层代理)。
      • 这意味着 HAProxy 只是单纯地转发 TCP 数据包,不会去解析 HTTP 协议的内容(比如它看不懂 HTTP 头部,不知道 URL 是什么,也看不到 Cookie)。它只认 IP 和端口。
    • use_backend test-http-nodes:
      • 这是核心动作指令:告诉前端,“所有发到我这里的流量,请全部转交给名为 test-http-nodes 的后端去处理。”
      • (注:因为这里是全局转发,没有写复杂的 ACL规则。如果是 HTTP 模式,这里通常配合 acl 规则实现根据域名或路径分发。)

    二、 Backend(后端模块):负责“干活”

    代码位置: 第 47 – 52 行

    haproxy

    backend test-http-nodes
        mode tcp
        default-server inter 1000 weight 6
        server web1 192.168.226.20:80 check inter 3000 fall 3 rise 5 weight 2 addr 192.168.226.20 port 3306
        server web2 192.168.226.30:80 check inter 3000 fall 3 rise 5

    后端模块的作用是定义真实的后端服务器集群,以及如何对它们进行负载均衡和健康检查。

    • backend test-http-nodes:

      • 定义了一个名为 test-http-nodes 的后端。这个名字必须和前面前端里 use_backend 指定的名字完全一致,这样才能对接上。
    • mode tcp:

      • 同样设置为 TCP 模式,必须和前端的模式保持一致(或兼容)。
    • default-server inter 1000 weight 6:

      • 这是给下面所有 server 设置的默认参数。
      • 意思是:如果在下面的 server 行里没有单独写健康检查间隔(inter),默认就是 1000 毫秒检查一次;如果没有单独写权重(weight),默认权重是 6。
    • server web1 ...(第一台后端服务器):

      • web1: 服务器名称。
      • 192.168.226.20:80: 声明服务器地址是 192.168.226.20,端口是 80。这是 HAProxy 尝试连接的初始目标。
      • check: 开启对该服务器的健康检查。
      • inter 3000: 覆盖了默认值。实际检查间隔是 3000 毫秒(3秒)。
      • fall 3: 连续失败 3 次,判定该服务器宕机(剔除)。
      • rise 5: 连续成功 5 次,判定该服务器恢复(重新加入集群)。
      • weight 2: 覆盖了默认值。该服务器的权重是 2(数值越小,分配到的流量比例越小)
      • addr 192.168.226.20 port 3306: 这是这行代码最大的疑点/坑点!
      • 在 HAProxy 中,如果在 server 名字后面写了 IP 和端口,再使用 addr 和 port 参数,HAProxy 会以后面的 addr 和 port 为准!
      • 也就是说,HAProxy 虽然看到你写了 :80,但最终它会去连接 192.168.226.20 的 3306 端口(通常是MySQL数据库端口),而不是 80 端口。
    • server web2 ...(第二台后端服务器):

      • 地址是 192.168.226.30:80。
      • 健康检查参数和 web1 一样(除了权重未指定,会继承默认的 weight 6)。

    整体工作流:

    1. 客户端访问 192.168.226.10:80。
    2. 被 frontend test-http 捕获。
    3. 前端把请求转给 backend test-http-nodes。
    4. 后端中有两台服务器 web1 和 web2。HAProxy会根据权重和健康状态,在四层(TCP)将请求分发给这两台服务器。

    image-20260922185955609

    核心结论:*:80 的绑定逻辑

    可以用一句话精准概括:

    当 HAProxy 配置了 bind *:80 时,只要目标 IP 是这台 HAProxy 服务器自身拥有的(配置在网卡上的),无论通过哪个 IP 访问其 80 端口,HAProxy 都会接管并处理该请求。

    详细拆解说明:

    1. 什么是“HAProxy 上有的 IP”? 就是指在这台 HAProxy 机器的操作系统里(通过 ip addr 或 ifconfig 命令能看到的)实际配置的 IP 地址。通常包括:

    • 对外业务 IP:例如 192.168.226.10(外部浏览器和客户端用它来访问)。
    • 本机回环 IP:例如 127.0.0.1(在本机内部执行 curl localhost 时使用)。
    • 如果机器上还有其他虚拟网卡(如 Docker 网桥 IP 172.17.0.1),也包含在内。

    2. * 号的作用范围: * 是通配符,代表“这台机器上的所有本地 IP”。HAProxy 会在所有这些 IP 的 80 端口上竖起耳朵监听。

    3. 界限非常明确(仅限于此): 如果外部访问的 IP不是这台 HAProxy机器上配置的IP(比如您去访问 192.168.226.99:80,而该 IP 并不在HAProxy 的网卡上),这个网络请求根本不会到达HAProxy机器,自然也就无法被 *:80 接管,会直接超时或报错。

    4. 端口限定(只限 80): *:80 仅仅接管 80 端口,绝对不会暴露机器上的其他端口(如 SSH 的 22,数据库的 3306)。

    一句话实战总结:

    在您的实验环境中,写成 bind *:80,就相当于开启了“全频道接收”模式。无论是别人从局域网通过 192.168.226.10:80 找过来,还是您自己在机器里通过 127.0.0.1:80 找过来,HAProxy 都会一视同仁

    image-20260922192350668

    image-20260922192407153

    image-20260922193550894


    listen替代frontend+backend

    使用listen替换上面的frontend和backend的配置方式,可以简化设置,通常只用于TCP协议的应用
    listen  web_port_81
        bind *:81
        mode http
        option  forwardfor
        # 这个选项的作用是启用或禁用将原始客户端的IP地址添加到HTTP请求的X-Forwarded-For头部中。
        server web1   192.168.226.20:80   check inter 3000 fall 3 rise 5
        server web2   192.168.226.30:80   check inter 3000 fall 3 rise 5
    
    [root@haproxy ~]# systemctl restart haproxy

    image-20260922200712151

    image-20260922201135267

    image-20260922201530982

    HAProxy调度算法

    HAProxy通过固定参数balance指明对后端服务器的调度算法,该参数可以配置在listen或backend选项中。

    HAProxy的调度算法分为静态和动态调度算法,但是有些算法可以根据参数在静态和动态算法中相互转换。

    官方文档:http://cbonte.github.io/haproxy-dconv/2.1/configuration.html#4-balance

    静态算法

    静态算法:按照事先定义好的规则轮询公平调度,不关心后端服务器的当前负载、链接数和响应速度等,且无法实时修改权重,只能靠重启HAPr

    动态算法

    动态算法:基于后端服务器状态进行调度适当调整,优先调度至当前负载较低的服务器,且权重可以在haproxy运行时动态调整无需重启。

    ==算法很多不列举了,需要自己根据情况选择==

    listen  web_host
      bind 192.168.226.10:80
      mode http
      log global
      balance static-rr        #就是在 balance 后面跟上算法名称
      server web1  192.168.226.20:80 weight 1 check inter 3000 fall 2 rise 5
      server web2  192.168.226.30:80 weight 2 check inter 3000 fall 2 rise 5
  • linux的Apache

    httpd 命令

    httpd 为 Apache HTTP 服务软件的命令

    相关文件

    文件 说明
    /etc/httpd/conf/httpd.conf 主配置文件
    /etc/httpd/conf.d/ 存放虚拟主机配置文件
    /etc/httpd/conf.modules.d/ 存放模块配置文件`
    /etc/httpd/modules/ 存放模块文件
    /var/log/httpd/ 存放日志文件
    /var/www/html/ 存放默认的网页文件

    主配置文件(/etc/httpd/conf/httpd.conf)

    image-20260805190131257

    ==理解==:主配置文件,配置权限的目的

    Apache的权限配置 = 一套“门禁系统”,决定哪些文件夹能进、哪些不能进、进去后能干嘛。


    1. 先看懂你的配置文件在说什么

    你贴的配置里有三个<Directory>块,它们按从最严格到最宽松的顺序排列:

    ① 根目录(<Directory />)—— 全局“一刀切”禁止

    apache

    <Directory />
        AllowOverride none
        Require all denied
    </Directory>

    翻译成人话: “整个Linux根目录(/)下的所有文件,默认统统不许访问!” 这是Apache的安全底线——万一有人想通过URL读取你的/etc/passwd或/root/,直接拦死。


    ② 你的网站根目录(<Directory "/data/www/html">)—— 白名单放行

    apache

    <Directory "/data/www/html">
        AllowOverride none
        Require all granted
    </Directory>

    翻译成人话: “虽然根目录全禁了,但这个特定的文件夹例外——所有人(all)都可以访问里面的文件(granted)。”

    这就是你网站能正常打开的原因:Apache检查权限时,会选最匹配的那个<Directory>,而不是叠加所有规则。


    ③ /var/www 和 /var/www/html(中间两个)—— 兼容老路径

    这两个块是为了兼容RHEL/CentOS系统的默认网站目录(老版本Apache默认用/var/www/html)。 你既然改成了/data/www/html,这两个块其实没用上,但留着也没坏处。


    2. 为什么需要这套“门禁系统”?(背后的安全逻辑)

    如果你不给/data/www/html加Require all granted,Apache会继承根目录的Require all denied,结果就是:

    你浏览器访问 http://IP/index.html → Apache查权限表 → 发现/被禁止 → 直接返回403 Forbidden

    这就是初学者最常见的“配好路径却打不开网站”的根源——你配了根目录,但忘了给具体文件夹开门。


    3. AllowOverride none 是啥意思?(很多人忽略的坑)

    你配置里写了AllowOverride none,意思是:

    禁止这个目录下的 .htaccess 文件覆盖当前权限设置。

    • 如果设为All,Apache会去读取/data/www/html/.htaccess,并允许它临时修改权限(比如拒绝某个IP)。
    • 设为None,Apache直接忽略.htaccess,提升性能(不用每次请求都去磁盘找这个文件)。

    实战建议:生产环境永远用None,把权限写在主配置文件里,既安全又快速。


    4. 用“门禁卡”模型彻底吃透权限配置

    想象你进入一栋大楼(Apache服务器):

    配置项 比喻
    <Directory /> 大楼的总闸:晚上10点后全楼封锁
    <Directory "/data/www/html"> 你工位的门禁卡:只有这张卡能刷开特定办公室
    Require all granted 门禁卡权限设为“任何人都能进”
    Require all denied 门禁卡权限设为“天王老子也别进”
    AllowOverride none 禁止办公室员工自己改门锁密码(防乱改)

    虚拟主机

    Httpd 支持在一台物理主机上实现多个网站,即多虚拟主机

    • 网站的唯一标识,多虚拟主机有三种实现方案
      • IP 相同 + Port 不同
      • IP 不同 + Port 相同
      • FQDN 不同

    image-20260805221722847

    给网卡添加临时ip

    给网卡添加IP地址 [root@server1 ~]# nmcli connection modify ens160 +ipv4.addresses “192.168.226.128 /24” [root@server1 ~]# nmcli connection modify ens160 +ipv4.addresses “192.168.226. …. /24” [root@server1 ~]# nmcli connection modify ens160 +ipv4.addresses “192.168.226. …. /24”

    image-20260805223616673

    image-20260805224319406

    1. 基于多IP加一个端口号

    image-20260806081535917

    image-20260806081601513


    服务日志

    httpd 有两种常见日志类型:==访问日志和错误日志==,==错误日志==使用标准 syslog 级别,==按严重性递增排序==。

    日志配置文件

    主配置文件日志配置相关定义块

    image-20260806084026946

    不同工具访问

    第一部分:日志格式解释(每列代表什么?)

    这条日志使用的是标准的 Combined Log Format(组合日志格式)。按照从左到右的顺序,各个字段的含义如下:

    1. 192.168.226.1:客户端 IP 地址。这是发起请求的电脑(也就是您的本机)的IP。
    2. -:远程登录名(Identd)。因为大多数服务器不启用身份验证检查,所以通常是横杠“-”。
    3. -:认证用户名。如果您的网站需要HTTP基本认证(输入账号密码),这里会显示登录的用户名;否则也是横杠“-”。
    4. [05/Aug/2026:12:02:10 +0800]:时间戳。
      • 格式为 [日/月/年:时:分:秒 时区]。
      • 也就是 2026年8月5日,12点02分10秒,东八区(北京时间)。
    5. "GET / HTTP/1.1":请求行。
      • GET:请求方法(获取网页)。
      • /:请求的路径(即网站根目录)。
      • HTTP/1.1:使用的HTTP协议版本。
    6. 200:HTTP 状态码。表示请求成功,服务器返回了内容(200是成功,404是找不到页面,408是请求超时等)。
    7. 7829:响应体大小(字节)。服务器返回给客户端的网页数据量(不含响应头)。
    8. "Mozilla/5.0 ...":User-Agent(用户代理)。这是你问的第二个问题的关键。它告诉服务器客户端是什么浏览器或工具。

    第二部分:为什么最后 curl 和浏览器显示的日志不一样?

    在截图的最底部,您会看到前几行是 curl 发起的请求,最后一行突然变成浏览器请求了。它们的区别主要体现在以下两点:

    1. 状态码和请求路径不同(404 vs 200)

    • 最后一行(浏览器):"GET /favicon.ico HTTP/1.1" 404 ...。这是浏览器在加载完网页后,自动去请求网站图标(favicon.ico)。因为您的服务器上可能没有这个图标文件,所以返回了 404 Not Found。
    • 上面的 curl 行:"GET / HTTP/1.1" 200 ...。您用 curl 默认请求的是根路径 /,且服务器找到了页面,所以返回的是 200 OK。

    2. User-Agent 不同(这一点最重要)

    • 浏览器(末尾行):User-Agent 非常长,包含 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.0.0。这说明是 Windows 10 上的 Edge 浏览器 (Chromium内核) 发送的请求。
    • 上面的 curl 行:User-Agent 是 curl/7.76.1。curl 是命令行工具,它发出来的请求通常会有非常明显的标识,告诉服务器“我是个命令行工具,不是用户浏览器”。

    为什么看起来不一样? 因为Web服务器记录日志时,就是如实记录客户端发来的 HTTP 请求头。

    • 浏览器访问网页时,会发送详细的浏览器版本和操作系统信息。
    • 命令行 curl 默认只发送最简短的标识 curl/版本号。

    总结: 格式是完全标准的 Access Log。底部的变化只是因为发起请求的工具不同(curl 请求了 /,而浏览器随后又自动请求了 /favicon.ico),并且它们各自携带的“身份标识(User-Agent)”不同,服务器只是把收到的东西如实记录下来了。


    image-20260806084240417

    image-20260806085203107

    三种网卡模式上网详解

    image-20260806102513737

    image-20260806102523015

    image-20260806102538824

    ==添加json格式的日志==

    image-20260806103726069

        LogFormat "{n
        "type": "apache_access",n
        "timestamp": "%{%Y-%m-%dT%H:%M:%S%z}t",n
        "client_ip": "%a",n
        "method": "%m",n
        "path": "%U",n
        "query": "%q",n
        "status": %>s,n
        "response_size": %b,n
        "referer": "%{Referer}i",n
        "user_agent": "%{User-Agent}i",n
        "response_time_ms": %D,n
        "server_name": "%V"n
    },n" json

    image-20260806104625605


    URL匹配规则

    Apache httpd的URL匹配规则主要包括以下几种指令:

    1. <Location> : 基于URL路径进行匹配。
    2. <LocationMatch> : 使用正则表达式进行URL路径匹配。
    3. <Directory> : 基于服务器文件系统的目录路径进行匹配。
    4. <DirectoryMatch> : 使用正则表达式进行目录路径匹配。
    5. <Files> : 基于文件名进行匹配。
    6. <FilesMatch> : 使用正则表达式进行文件名匹配。

    具体用法见课件,不用记,本质就是匹配格式用到看一下


    Options 指令和Override指令

    相关介绍

    用于控制特定目录下启用的服务器功能特性(如目录列表、符号链接等)

    语法:Options [+|-]option [[+|-]option] ... 作用域:<Directory>, <Location>, <Files>, .htaccess 特性:

    • +/- 前缀:动态添加或移除选项,支持配置继承与合并(无前缀时直接覆盖)
    • 优先级:子目录配置优先于父目录,正则表达式匹配的配置最后生效

    常用选项

    选项 功能说明 适用场景
    Indexes 当目录无默认文件(如 index.html)时,自动生成文件列表 开发环境调试,生产慎用
    FollowSymLinks 允许通过符号链接访问目录外资源(在 <Location> 中无效) 跨目录资源整合
    MultiViews 内容协商:根据请求头(如语言)自动匹配文件(需 mod_negotiation) 多语言站点适配
    ExecCGI 允许执行 CGI 脚本(需 mod_cgi) 动态脚本处理(如 Perl/Python)
    Includes 启用服务端包含(SSI)(需 mod_include) 动态页面片段嵌入
    IncludesNOEXEC 允许 SSI 但禁用 #exec 执行命令/CGI 安全限制下的 SSI 功能
    SymLinksIfOwnerMatch 仅当符号链接与目标文件所有者相同时允许访问(在 <Location> 中无效) 高安全要求的共享环境
    All 启用除 MultiViews 外的所有特性(默认值) 快速启用常用功能
    None 禁用所有额外特性 严格安全策略

    image-20260806141227056

    实践

    把 index.html 备份.bak文件,然后浏览器默认不返回响应报文,只返回服务端的原始样子

    image-20260806142042538

    image-20260806142057182

    案例分析

    使用 Index 和 FollowSymLinks,当访问无索引文件的目录时显示文件列表,允许符号链接跳转;依次去掉 FollowSymLinks 和 Index 观察现象。

    # 准备实验环境
    [root@localhost ~]# mkdir -pv /data/site04/public
    [root@localhost ~]# touch /data/site04/public/{1,2,3}.html
    [root@localhost ~]# ln -s /etc/passwd /data/site04/public/passwd.tx
    [root@localhost ~]# cat << EOF > /etc/httpd/conf.d/site01.conf
    Listen 8001
    <Virtualhost 172.16.175.129:8001>
       Documentroot /data/site04
       <Directory /data/site04/public>
          Options Indexes FollowSymLinks
          Require all granted
       </Directory>
    </Virtualhost>
    EOF
    [root@localhost ~]# systemctl restart httpd
    # 通过浏览器访问测试验证

    image-20260806191826191

    image-20260806191942499


    AllowOverride 指令

    相关介绍

    用于控制是否允许在 .htaccess 文件中覆盖主配置文件的设定,通常用于目录级的动态配置,无需重启服务即可生效。作用域限制为:仅能在 <Directory> 块中生效,在 <Location>、<Files> 等配置段中无效。

    常用参数

    参数值 说明
    None 完全忽略 .htaccess 文件,服务器不读取其内容(默认推荐,提升性能与安全性)。
    All 允许 .htaccess 覆盖所有支持的指令(需谨慎启用,可能引发安全风险)。
    AuthConfig 允许覆盖认证相关指令(如 AuthName, Require)。
    FileInfo 允许覆盖文件处理指令(如 ErrorDocument, RewriteRule, Header)。
    Indexes 允许覆盖目录索引控制指令(如 DirectoryIndex, IndexIgnore)。
    Limit 允许覆盖访问控制指令(如 Allow, Deny, Order)。
    Options[=…] 允许覆盖目录特性控制指令(如 Options FollowSymLinks,可指定具体允许的选项)。

    案例分析

    通过 AllowOverride 指令和 .htaccess 文件,动态为指定目录设定特性控制指令

    # 准备实验环境
    [root@localhost ~]# mkdir -pv /data/site05/public
    [root@localhost ~]# touch /data/site05/public/{1,2,3}.html
    [root@localhost ~]# cat << EOF > /etc/httpd/conf.d/site02.conf
    Listen 8002
    <Virtualhost 172.16.175.129:8002>
       Documentroot /data/site05
       <Directory /data/site05/public>
          AllowOverride Options=Indexes
          Require all granted
       </Directory>
    </Virtualhost>
    EOF
    [root@localhost ~]# systemctl restart httpd
    # 通过浏览器访问:禁止访问
    # 通过 .htaccess 文件覆盖设定
    [root@localhost ~]# echo "Options Indexes" > /data/site05/public/.htaccess
    # 通过浏览器访问测试验证

    ==理解==:

    1. 服务器启动时,主配置文件(site02.conf)已经加载,AllowOverride Options=Indexes 已经生效。
    2. 如果修改主配置文件,需要重启 httpd 服务,这会影响业务连续性。
    3. 为了不重启服务,可以使用 .htaccess 文件在运行中动态覆盖指定目录的配置。
    4. AllowOverride 后面跟的选项(如 Options=Indexes)限定了哪些指令可以被覆盖,未列出的指令即使写在 .htaccess 里也无效。
    5. 在 .htaccess 中必须重新声明 Options Indexes,不能只写 Indexes,因为 .htaccess 里写的是完整的 Apache 配置指令语法。

    AllowOverride 定义了 .htaccess 的“权限边界”,而 .htaccess 是 Apache 实现“不停机动态配置”的核心机制,两者配合可实现细粒度的目录级策略控制。


    ==实践==:

    image-20260806201734696

    image-20260806201821394

    image-20260806203112505


    访问控制

    针对各种资源,通常基于两种方式:客户端来源地址和用户账号来进行访问控制

    基于IP地址

    基于客户端IP地址的访问控制:

    • Apache 2.4 默认 拒绝所有未显式授权 的访问,需明确配置 Require all granted 开放全局访问
    • Require host 可基于域名控制(如 Require host example.com),但需注意 DNS 解析延迟可能影响性能
    • 不带 not 的多个 Require 是“或”关系(任一满足即可)
    • 带 not 和不带 not 的语句混合时是“与”关系(需同时满足)

    常见语法:

    • 允许所有主机访问:Require all granted
    • 拒绝所有主机访问:Require all denied
    • 授权/拒绝指定来源的IP/host访问:Require [not] <ip|host> <ipaddress|hostname>

    组合策略配置:

    • <RequireAll> 内部所有条件必须同时满足(逻辑与)
    • <RequireAny> 内部任一条件满足即可(逻辑或)

    基于用户

    基于用户的访问控制主要通过身份验证(Authentication)和授权(Authorization)机制实现:

    认证类型:

    • Basic 认证:用户名密码以 Base64 编码传输(明文,需配合 HTTPS 保证安全)
    • Digest 认证:密码哈希传输(更安全,但配置复杂)

    核心配置指令:

    • AuthName:定义认证区域名称(浏览器弹窗提示文本)
    • AuthUserFile:指定存储用户名密码的文本文件路径(如 .htpasswd)
    • Require:授权规则(如 valid-user 允许所有认证用户,或指定用户/组)

    image-20260806235310220

    image-20260806235505859

    ==特殊文件==:. file以点(.)开头的文件是隐藏文件,要用 -a 参数才可看到

    虚拟主机配置

    apache

    <VirtualHost 192.168.226.131:8007>

    意思:定义一个虚拟主机,绑定到IP 192.168.226.131 的 8007 端口 → 相当于在这台服务器上单独开了一个网站

    启用密码认证

    apache

    AuthType Basic
    AuthName "FBI warning"
    AuthUserFile "/etc/httpd/.htpasswd"
    配置项 意思
    AuthType Basic 使用基础密码认证(弹出登录框)
    AuthName "FBI warning" 登录框显示的提示文字(”FBI警告”)
    AuthUserFile "/etc/httpd/.htpasswd" 密码文件位置,里面存了用户名和加密密码

    image-20260807002902856

    image-20260807002940874

    image-20260807003010188


    URL 处理机制

    别名(Alias) 和 重定向(Redirect) 是两种不同的 URL 处理机制,核心区别在于是否改变客户端浏览器地址栏的 URL 以及是否触发新的 HTTP 请求

    别名 Alias

    用于将 URL 路径映射到文件系统的不同位置,实现灵活的资源管理和访问控制。

    路径资源解耦:将 URL 路径(如 /images)映射到物理目录(如 /var/www/media),无需修改实际文件位置,示例如下:

    Alias "/images" "/var/www/html/media"
    访问 http://domain.com/images/photo.jpg 实际返回 /var/www/html/media/photo.jpg

    简化URL结构:隐藏复杂路径,提升用户体验和 SEO 友好性 多域名/路径管理:同一服务器托管多个站点,示例如下:

    <VirtualHost *:80>
        ServerName blog.example.com
        Alias "/" "/var/www/blog"
    </VirtualHost>
    <VirtualHost *:80>
        ServerName notes.example.com
        Alias "/" "/var/www/notes"
    </VirtualHost>
    ...

    案例分析

    将 /usr/share/httpd/icons 独立资源通过 /icons 别名暴露简洁路径

    1. Alias /icons /usr/share/httpd/icons

    • 作用:这是路径别名。
    • 说明:它把访问路径 /icons 映射到服务器硬盘上的真实物理路径 /usr/share/httpd/icons。
    • 举例:如果用户在浏览器访问 http://192.168.226.128:8007/icons/abc.png,Apache 会去服务器的 /usr/share/httpd/icons/abc.png 目录下找这个图片。

    image-20260807101330671

    [root@localhost ~]# vim  /etc/httpd/conf.d/site01.conf  # 配置子配置文件
    Listen 8007
    <Virtualhost 192.168.226.128:8007>
       Alias /icons /usr/share/httpd/icons
       <Directory /usr/share/httpd/icons>
          Options Indexes FollowSymLinks
          AllowOverride none
          Require all granted
       </Directory>
    </Virtualhost>
    # 通过浏览器访问 http://192.168.226.128:8007/icons/
    # [root@localhost ~]# curl -I http://192.168.226.128:8007/icons/ 返回 200

    image-20260807102403777


    重定向 Redirect

    强制客户端跳转到另一个 URL,通常用于路径迁移或权限控制

    相关介绍

    网站改版迁移:旧路径 http://old.com 永久跳转到 http://new.com(301) 权限控制:未登录用户访问 /admin 时跳转到登录页(302) 临时维护:将流量临时导向通知页面(302)

    案例分析1

    网站改版迁移:旧域名访问新网站

    [root@localhost ~]# mkdir -pv /data/site02/ 
    [root@localhost ~]# echo "This is New NO.2 website!" > /data/site02/index.html
    [root@localhost ~]# vim /etc/httpd/conf.d/site02.conf
    # old site 
    <Virtualhost 192.168.226.128>
       ServerName cloud.eagleslabtest.com
       Redirect 301 / http://ncloud.eagleslabtest.com/
    </Virtualhost>
    
    # new site
    <Virtualhost 192.168.226.128>
       ServerName ncloud.eagleslabtest.com
       Documentroot /data/site02
       <Directory /data/site02>
          Require all granted
       </Directory>
    </Virtualhost>
    
    [root@localhost ~]# echo "192.168.226.128 ncloud.eagleslabtest.com" >> /etc/hosts
    [root@localhost ~]# echo "192.168.226.128 cloud.eagleslabtest.com" >> /etc/hosts      # 在linux操作系统配置dns域名解析服务
    [root@localhost ~]# systemctl restart httpd
    # 测试验证
    [root@localhost ~]# curl -L http://cloud.eagleslabtest.com/index.html
    This is New NO.8 website!
    [root@localhost ~]# curl -I http://cloud.eagleslabtest.com/index.html
    HTTP/1.1 301 Moved Permanently
    Date: Mon, 07 Jul 2025 19:20:27 GMT
    Server: Apache/2.4.62 (Rocky Linux)
    Location: http://ncloud.eagleslabtest.com/index.html
    Connection: close
    Content-Type: text/html; charset=iso-8859-1

    image-20260807145458842

    image-20260807145513691

    image-20260807145747296


    网页压缩技术

    网页压缩技术主要通过 mod_deflate 模块实现,它能显著减少传输数据量,提升网站加载速度和用户体验

    相关介绍

    • 使用 mod_deflate 模块压缩页面优化传输速度
    • 适用场景
      • 节约带宽,额外消耗CPU;同时,可能有些较老浏览器不支持
      • 压缩适于压缩的资源,例如文本文件

    案例分析

    # 准备实验环境
    [root@localhost ~]# mkdir -pv /data/site03/ /data/site04/
    [root@localhost ~]# tree /etc > /data/site03/blog.html
    [root@localhost ~]# tree /etc > /data/site04/blog.html
    # 配置对比vhost
    [root@localhost ~]# vim  /etc/httpd/conf.d/site03.conf
    Listen 8009
    Listen 8010
    <Virtualhost *:8009>
       Documentroot /data/site03/
       <Directory /data/site03/>      # 这个设置网页压缩技术
          Require all granted
       </Directory>
       AddOutputFilterByType DEFLATE text/plain
       AddOutputFilterByType DEFLATE text/html     
       SetOutputFilter DEFLATE
    </Virtualhost>
    
    <Virtualhost *:8010>
       Documentroot /data/site04/
       <Directory /data/site04/>
          Require all granted
       </Directory>
    </Virtualhost>
    
    [root@localhost ~]# systemctl restart httpd
    # 默认curl没有压缩,需要加参数 --compressed,输出结果关注 Content-Encoding: gzip & Content-Length 
    [root@localhost ~]# curl -I --compressed 127.0.0.1:8009/blog.html
    [root@localhost ~]# curl -I --compressed 127.0.0.1:8010/blog.html

    本地调试和验证,不依赖网络

    服务器在运行 Apache 服务后,首先要确认服务是否在本机正常运行。

    • 使用 127.0.0.1 不需要经过网卡、路由器或防火墙,数据直接在系统内核层面流转。
    • 这可以排除很多网络层面的问题(比如防火墙阻挡了外部访问、路由配置错误等)。如果 curl 127.0.0.1 能通,说明 Apache 本身是好的。

    2. 适用于当前的上下文(同一台机器)

    截图中的操作是在服务器本机的终端里进行的。

    • 因为您已经在服务器里([root@localhost conf.d]#),您访问自己机器上的服务,最稳妥、最快的方式就是用 127.0.0.1。
    • 如果换成公网 IP(也就是 192.168.226.128),在某些云服务器或设置了强防火墙的环境下,可能需要额外开启端口权限才能访问。

    image-20260807154343341

    image-20260807154537776


    HTTPS

    Web网站的登录页面都是使用HTTPS加密传输的,加密数据以保障数据的安全,HTTPS能够加密信息,以免敏感信息被第三方获取,所以很多银行网站或电子邮箱等等安全级别较高的服务都会采用HTTPS协议.

    采用HTTPS协议的服务器必须要有一套证书,可以通过权威机构申请,也可以自己制作,目前国内很多网站都自己做的,当你访问一个网站的时候提示证书不可信任就表示证书是自己做的

    案例分析

    自建 HTTPS 服务器:自签名证书生成、Apache 配置、强制 HTTP 跳转 HTTPS及测试验证

    [root@localhost ~]# mkdir -pv /data/site01/
    [root@localhost ~]# echo "This is NO.1 website!" > /data/site01/index.html
    # 安装 mod_ssl 模块和 openssl 工具
    [root@localhost ~]# yum install mod_ssl openssl -y
    # 确认 httpd 已经加载 ssl_module 模块
    [root@localhost ~]# httpd -M | grep ssl
    ssl_module (shared)
    # 备份默认 ssl.conf 文件避免互相影响
    [root@localhost ~]# mv /etc/httpd/conf.d/ssl.conf  /etc/httpd/conf.d/ssl.conf.bak   # 把文件备份,备份成.bak文件
    [root@localhost ~]# grep -nr 'ssl' /etc/httpd/conf.modules.d/*
    /etc/httpd/conf.modules.d/00-ssl.conf:1:LoadModule ssl_module modules/mod_ssl.so
    # 创建 X509 自签名证书
    [root@localhost ~]# openssl req -x509 -nodes -days 365 -newkey rsa:2048 
       -keyout /etc/httpd/ssl/server.key 
       -out /etc/httpd/ssl/server.crt 
       -subj "/C=XX/ST=Test/O=DevEnv/CN=ssl.eagleslabtest.com" 
       -addext "subjectAltName=DNS:ssl.eagleslabtest.com,IP:192.168.226.128"
    
    # HTTPS 相关虚拟主机配置
    [root@localhost ~]# vim /etc/httpd/conf.d/site01.conf
    # 强制 HTTP 跳转 HTTPS
    <VirtualHost 192.168.226.128:80>
       ServerName ssl.eagleslabtest.com
       RewriteEngine On
       RewriteCond %{HTTPS} off
       RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
    </VirtualHost>
    
    Listen 443 https
    <VirtualHost 192.168.226.128:443>
       ServerName ssl.eagleslabtest.com
       DocumentRoot /data/site01/
       <Directory /data/site01/>
          Require all granted
       </Directory>
       SSLEngine on
       SSLCertificateFile /etc/httpd/ssl/server.crt
       SSLCertificateKeyFile /etc/httpd/ssl/server.key
    
       # 安全增强配置(可选但推荐)
       # 禁用旧协议
       SSLProtocol all -SSLv2 -SSLv3 -TLSv1 -TLSv1.1
       # 强加密套件
       SSLCipherSuite HIGH:!aNULL:!MD5:!3DES
    </VirtualHost>
    
    
    [root@localhost ~]# systemctl restart httpd
    [root@localhost ~]# echo "192.168.226.128  ssl.eagleslabtest.com" >> /etc/hosts
    # 测试验证: -k 忽略证书警告测试
    [root@localhost ~]# curl -k https://ssl.eagleslabtest.com/index.html
    This is NO.10 website!
    # 测试验证: 强制跳转
    [root@localhost ~]#  curl -k -L http://ssl.eagleslabtest.com/index.html
    This is NO.1 website!

    一、通过openssl工具来自己生成一个证书,然后颁发给自己

    # 1. 安装mod_ssl和openssl
    [root@localhost ~]# yum install mod_ssl openssl -y
    
    # 2.生成2048位的加密私钥
    [root@localhost ~]# openssl genrsa -out server.key 2048
    
    # 3.生成证书签名请求(CSR)
    [root@localhost ~]# openssl req -new -key server.key -out server.csr
    # 一路回车到底,过程暂时不需要管
    
    # 4.生成类型为X509的自签名证书。有效期设置3650天,即有效期为10年
    [root@localhost ~]# openssl x509 -req -days 3650 -in server.csr -signkey server.key -out server.crt
    
    # 5.复制文件到相应的位置
    [root@localhost ~]# cp server.crt /etc/pki/tls/certs/
    [root@localhost ~]# cp server.key /etc/pki/tls/private/     
    [root@localhost ~]# cp server.csr /etc/pki/tls/private/
    
    # 6.修改配置文件,指定我们自己的证书文件路径
    [root@localhost ~]# vim /etc/httpd/conf.d/ssl.conf
    SSLCertificateFile /etc/pki/tls/certs/server.crt
    SSLCertificateKeyFile /etc/pki/tls/private/server.key
    
    # 7.重启httpd
    [root@node1 ~]# systemctl restart httpd

    阶段一:基础环境与站点准备

    bash

    mkdir -pv /data/site10/
    echo "This is NO.10 website!" > /data/site10/index.html
    • mkdir -pv: -p 参数确保如果父目录(/data)不存在,则一并创建;-v 参数让创建过程可视化。
    • echo >: 创建了一个简单的测试页面,内容为 This is NO.10 website!。这是后续验证Web服务是否正常工作的基础文件。

    阶段二:安装与加载SSL模块

    bash

    yum install mod_ssl openssl -y
    httpd -M | grep ssl
    • mod_ssl: 这是Apache的SSL/TLS模块。没有它,Apache就无法处理HTTPS协议。
    • openssl: 这是一个强大的命令行工具包,我们后续会用它的 req 子命令来生成自签名证书。
    • httpd -M: 这个命令会列出Apache当前加载的所有静态和共享模块。grep ssl 用来确认 ssl_module (shared) 已成功加载。这步验证非常关键,避免了因模块未加载导致配置失败。

    阶段三:准备证书文件与配置

    1. 备份默认配置

    bash

    mv /etc/httpd/conf.d/ssl.conf{,.bak}
    • 系统自带的 ssl.conf 是默认的HTTPS配置示例。强烈建议备份或重命名它,因为如果不加修改直接使用,它可能会与你自己写的配置文件产生冲突(比如端口、证书路径等)。加 .bak 后缀是一个安全且常见的做法。

    2. 验证模块加载路径

    bash

    grep -nr 'ssl' /etc/httpd/conf.modules.d/*
    • 这步是双重确认。conf.modules.d 目录下的 00-ssl.conf 文件确保了 mod_ssl.so 会被加载。这保证了安装是成功的。

    3. 生成X.509自签名证书

    bash

    openssl req -x509 -nodes -days 365 -newkey rsa:2048 
        -keyout /etc/httpd/ssl/server.key 
        -out /etc/httpd/ssl/server.crt 
        -subj "/C=XX/ST=Test/O=DevEnv/CN=ssl.eagleslabtest.com" 
        -addtext "subjectAltName=DNS:ssl.eagleslabtest.com,IP:172.16.175.129"

    这是核心命令,我们拆解一下每个参数的意义:

    • req: 用于处理证书请求和生成自签名证书的子命令。
    • -x509: 告诉OpenSSL直接输出一个自签名的X.509格式证书,而不是生成一个待签名的证书请求(CSR)。
    • -nodes: No DES。意思是“不加密私钥”。如果不加此参数,系统会要求你设置一个密码来保护私钥文件,但每次启动Apache时都需要手动输入密码。在自动化运维中这是不可接受的,因此我们使用此选项让私钥无密码保护。
    • -days 365: 证书有效期,这里设置为365天。
    • -newkey rsa:2048: 同时生成一个新的2048位RSA私钥和证书。
    • -keyout 与 -out: 分别指定生成的 私钥文件 和 证书文件 的存储路径。请确保 /etc/httpd/ssl/ 目录存在,否则命令会报错。
    • -subj: Subject。这是证书的“主题”信息,用 / 分隔各个字段。你这里设置了国家(C)、省份(ST)、组织(O)、和通用名称(CN)。
      • 在传统的SSL中,CN 必须与用户访问的域名完全一致。
    • -addtext: 这是最关键的一点。从2015年起,主流浏览器要求证书必须包含 SAN(Subject Alternative Name,主题备用名称) 字段。所以你在这里明确指定了该证书同时适用于域名 ssl.eagleslabtest.com 和IP 172.16.175.129。这使得你的测试环境在浏览器中能最大程度避免证书无效警告。

    image-20260807193532306

    image-20260807193545515

    image-20260807193552596

    image-20260807194034990

    image-20260807194254372

    LAMP架构介绍及案例

    MPM多路处理模块

    httpd 支持三种 MPM 工作模式:prefork, worker, event

    相关配置文件

    [root@localhost ~]# cat /etc/httpd/conf.modules.d/00-mpm.conf |grep mpm
    # LoadModule mpm_prefork_module modules/mod_mpm_prefork.so
    # LoadModule mpm_worker_module modules/mod_mpm_worker.so
    LoadModule mpm_event_module modules/mod_mpm_event.so

    prefork 模式

    Prefork MPM,这种模式采用的是预派生子进程方式,用单独的子进程来处理请求,子进程间互相独立,互不影响,大大的提高了稳定性,但每个进程都会占用内存,所以消耗系统资源过高。

    image-20260807200222373

    worker 模式

    Worker MPM 是Apche 2.0 版本中全新的支持多进程多线程混合模型的 MPM,由于使用线程来处理 HTTP 请求,所以效率非常高,而对系统的开销也相对较低,Worker MPM 也是基于多进程的,但是每个进程会生成多个线程,由线程来处理请求,这样可以保证多线程可以获得进程的稳定性。

    image-20260807200333643

    event 模式

    这个是 Apache 中最新的模式,在现在版本里的已经是稳定可用的模式。它和 worker 模式很像,最大的区别在于,它解决了 keepalive 场景下 ,长期被占用的线程的资源浪费问题(某些线程因为被keep-alive,挂在那里等待,中间几乎没有请求过来,一直等到超时)。

    event MPM 中,会有一个专门的线程来管理这些 keepalive 类型的线程,当有真实请求过来的时候,将请求传递给服务线程,执行完毕后,又允许它释放。这样,一个线程就能处理多个请求,实现了异步非阻塞。

    event MPM 在遇到某些不兼容的模块时会失效,将会回退到 worker 模式,一个工作线程处理一个请求。官方自带的模块,全部是支持event MPM的。

    image-20260807200502455

    ==MPM多路处理模块配置文件==:

    image-20260807201058105

    image-20260807204925499


    LAMP架构

    LAMP就是由Linux+Apache+MySQL+PHP组合起来的架构

    并且Apache默认情况下就内置了PHP解析模块,所以无需CGI即可解析PHP代码

    请求示意图:

    image-20260807200605179

    安装Apache

    [root@localhost ~]# yum install -y httpd
    
    # 启动httpd
    [root@localhost ~]# systemctl enable --now httpd
    
    # 关闭防火墙和SElinux
    [root@localhost ~]# systemctl stop firewalld
    [root@localhost ~]# setenforce 0

    安装PHP环境

    1. 安装php8.0全家桶
    [root@localhost ~]# yum install -y php*
    
    # 启动php-fpm
    [root@localhost ~]# systemctl enable --now php-fpm
    [root@localhost ~]# systemctl status php-fpm
    ● php-fpm.service - The PHP FastCGI Process Manager
         Loaded: loaded (/usr/lib/systemd/system/php-fpm.service; enabled; preset: disabled)
         Active: active (running) since Sat 2025-01-18 21:54:09 CST; 7s ago
       Main PID: 34564 (php-fpm)
         Status: "Ready to handle connections"
          Tasks: 6 (limit: 10888)
         Memory: 22.6M
            CPU: 119ms
         CGroup: /system.slice/php-fpm.service
                 ├─34564 "php-fpm: master process (/etc/php-fpm.conf)"
                 ├─34565 "php-fpm: pool www"
                 ├─34566 "php-fpm: pool www"
                 ├─34567 "php-fpm: pool www"
                 ├─34568 "php-fpm: pool www"
                 └─34569 "php-fpm: pool www"
    
    Jan 18 21:54:09 localhost.localdomain systemd[1]: Starting The PHP FastCGI Process Manage>
    Jan 18 21:54:09 localhost.localdomain systemd[1]: Started The PHP FastCGI Process Manager.
    1. 重启httpd服务以加载PHP相关模块
    [root@localhost ~]# systemctl restart httpd

    为什么安装php模块

    1️⃣ Apache 和 PHP 的关系

    text

    用户访问 index.php 
        ↓
    Apache (httpd) 收到请求
        ↓
    Apache 发现文件后缀是 .php
        ↓
    需要 PHP 模块来处理 → 模块在哪里?→ 在 httpd 配置文件中加载
        ↓
    PHP 模块执行 PHP 代码,返回 HTML 结果
        ↓
    Apache 把 HTML 返回给用户浏览器

    关键点:Apache 本身是”木头疙瘩”,只认识 HTML/静态文件。要让它处理 PHP,必须加载 mod_php(或通过 proxy_fcgi 转发给 php-fpm)。


    2️⃣ 重启加载了什么?

    当您执行 yum install -y php* 时,系统自动做了两件事:

    动作 说明
    ① 安装了 PHP 和 Apache 的 PHP 模块 比如 /etc/httpd/modules/libphp.so
    ② 在 Apache 配置中添加了加载指令 一般是 /etc/httpd/conf.d/php.conf 或 modules/ 目录下

    php.conf 内容示例:

    apache

    # 加载 PHP 模块
    LoadModule php_module modules/libphp.so
    
    # 让 Apache 识别 .php 文件
    AddHandler php-script .php
    DirectoryIndex index.php

    3️⃣ 为什么必须重启?

    Apache 的模块是在启动时一次性加载到内存的,运行过程中不会自动重新读取配置文件。

    text

    安装 PHP 模块 → 配置文件变了(新增 LoadModule)
         ↓
    不重启 → Apache 还在用旧的内存配置 → 不认识 PHP → 报错或下载 .php 文件
         ↓
    重启 → Apache 重新读取所有配置文件 → 加载新模块 → 能处理 PHP 请求 ✅

    安装MySQL数据库

    # 安装mariadb数据库软件
    [root@localhost ~]# yum install -y mariadb-server mariadb  # mariadb-server 算是一种数据库的一种
    
    # 启动数据库并且设置开机自启动
    [root@localhost ~]# systemctl enable --now mariadb
    
    # 设置mariadb的密码
    [root@localhost ~]# mysqladmin password '123456'
    
    # 验证数据库是否工作正常
    [root@localhost ~]# mysql -uroot -p123456 -e "show databases;"
    +--------------------+
    | Database           |
    +--------------------+
    | information_schema |
    | mysql              |
    | performance_schema |
    +--------------------+

    image-20260807211955223

    ⚠️ 一个小细节

    mariadb(客户端)其实是 mariadb-server 的依赖包,会自动安装。所以写成 yum install -y mariadb-server 就够了,后面那个 mariadb 是多余的,但写上也没错。

    mysqladmin 是什么?

    • 这是 MariaDB/MySQL 自带的管理工具(由客户端包提供)
    • 专门用来执行管理操作:改密码、创建/删除数据库、查看服务器状态、关闭服务等

    这条命令具体做了什么?

    text

    mysqladmin  →  连接到本地 MariaDB 服务(默认用 root 无密码)
         ↓
    password '123456' →  将 root 用户的密码设置为 123456

    ⚠️ 安全警告(重要!)

    bash

    mysqladmin password '123456'   # 这样写密码会暴露在命令历史中

    更安全的做法:

    bash

    mysqladmin password          # 不加密码,系统会交互式询问(输入时不可见)
    Enter password:              # 系统会提示输入新密码(更安全)
    New password: ********
    Confirm new password: ********

    拆解这条命令:

    参数 含义
    mysql 调用客户端工具
    -uroot 用 root 用户登录(-u 后面紧跟用户名,无空格)
    -p123456 密码是 123456(-p 后面紧跟密码,无空格)
    -e "show databases;" 执行 SQL 语句并退出(不进入交互式界面)

    输出的三个数据库解释:

    数据库名 作用
    information_schema 元数据仓库:存储所有数据库、表、列、权限等信息(只读,实际是内存虚拟表)
    mysql 系统核心库:存储用户账号、权限、时区、日志等关键配置数据
    performance_schema 性能监控库:收集数据库运行时的性能指标(CPU、IO、锁等待等),用于调优

    PHP探针测试

    在==默认的网站根目录下==创建info.php

    [root@localhost ~]# vim /var/www/html/info.php
    <?php
        phpinfo();
    ?>

    写一个简单的PHP代码,可以使用==phpinfo函数查看PHP的信息,==从而检测是否成功解析PHP代码

    编写好以后,我们访问:http://IP/info.php测试

    image-20260807212535325

    默认httpd服务端根目录就是/var/www/html,客户端根据url自己匹配

    image-20260818103841713

    数据库连接测试

    编写PHP代码,用PHP来连接数据库测试

    [root@localhost ~]# vim /var/www/html/mysql.php
    <?php
        $servername = "localhost";
        $username = "root";
        $password = "123456";
    
        // 创建连接
        $conn = mysqli_connect($servername, $username, $password);
    
        // 检测连接
        if (!$conn) {
             die("Connection failed: " . mysqli_connect_error());
        }
        echo "连接MySQL...成功!";
    ?>

    完整执行流程图

    text

    用户访问 http://IP/mysql.php
             ↓
    PHP 引擎执行代码
             ↓
    尝试连接 MySQL(localhost, root, 123456)
             ↓
        ┌────┴────┐
        ↓         ↓
     连接失败    连接成功
        ↓         ↓
    die() 输出  echo 输出
    错误信息    "连接成功"
        ↓         ↓
    脚本终止    脚本结束

    1️⃣ 一体机架构(您当前的学习环境)

    text

    ┌─────────────────────────┐
    │   一台服务器             │
    │  ┌──────┐ ┌──────────┐ │
    │  │Apache│ │  MySQL   │ │
    │  │ PHP  │ │MariaDB   │ │
    │  └──────┘ └──────────┘ │
    └─────────────────────────┘
             ↑
        用户访问(IP: 192.168.226.128)
    • 特点:Web 和数据库在同一台机器
    • 适用:学习、开发、小型个人网站
    • 缺点:资源竞争、单点故障、无法独立扩展

    2️⃣ 分离架构(生产环境标准)

    text

    ┌──────────────┐          ┌──────────────┐
    │  Web 服务器   │          │ 数据库服务器  │
    │  ┌────────┐  │  内网     │  ┌────────┐  │
    │  │Apache  │  │◄─────────►│  │ MySQL  │  │
    │  │ PHP    │  │ 高速连接  │  │MariaDB │  │
    │  └────────┘  │          │  └────────┘  │
    └──────────────┘          └──────────────┘
          ↑                           ↑
      用户访问                   数据存储
    • 特点:各自独立,通过网络通信
    • 适用:大多数企业级应用
    • 优点:资源独立、可分别扩展、更安全

    代码上的区别

    一体机(您当前的写法)

    php

    $servername = "localhost";  // 或 "127.0.0.1"

    因为数据库就在本机,直接用 localhost 连接。

    分离架构(生产环境写法)

    php

    $servername = "192.168.1.100";  // 数据库服务器的实际 IP
    // 或
    $servername = "db.example.com"; // 数据库服务器的域名

    ==Web 服务器通过网络连接到另一台机器上的数据库。==


    编写好以后,我们访问:http://IP/mysql.php测试:

    image-20260818111706164

    image-20260818111716248


    安装phpmyadmin

    由于我们还没有学习MySQL如何管理,我们可以部署phpmyadmin工具,该工具可以让我们可视化管理我们的数据库

    # 移动到网站根目录
    [root@localhost ~]# cd /var/www/html
    
    # 下载phpmyadmin源码
    [root@localhost ~]# wget https://files.phpmyadmin.net/phpMyAdmin/5.1.1/phpMyAdmin-5.1.1-all-languages.zip
    
    # 解压软件包,并且重命名
    [root@localhost ~]# unzip phpMyAdmin-5.1.1-all-languages.zip
    [root@localhost ~]# mv phpMyAdmin-5.1.1-all-languages phpmyadmin

    访问http://192.168.226.128/phpmyadmin测试:

    image-20260818113050966

    个人博客

    源码获取

    下载typecho博客系统源码到/var/www/html/typecho

    [root@localhost ~]# cd /var/www/html
    
    # 创建typecho目录
    [root@localhost ~]# mkdir typecho
    [root@localhost ~]# cd typecho
    
    [root@localhost ~]# wget https://github.com/typecho/typecho/releases/latest/download/typecho.zip
    
    # 解压源码
    [root@localhost ~]# unzip typecho.zip

    创建数据库

    点击创建数据库

    image-20260818124939893

    安装博客系统

    下面就可以开始进入网站安装的部分了,访问博客系统页面

    http://IP/typecho

    image-20260818125304894

    提示安装目录下面的/usr/uploads没有权限,那么我们手动赋予该目录w权限

    [root@localhost typecho]# chmod a+w usr/uploads/

    image-20260818125816377

    填写数据库信息,密码为部署mariadb时设置的123456

    image-20260818125906376

    image-20260818130341238

    image-20260818130758265

    image-20260818135300549

    ==LAMP==架构理解:

    1. LAMP 架构组件关系(“一体性”的实质)

    LAMP是一套提供完整 Web 服务处理闭环:

    • Linux:提供操作系统内核、进程调度、文件系统和网络协议栈,作为底层运行环境。
    • Apache HTTP Server:作为 Web 服务器进程,监听 HTTP 端口(默认 80),解析 HTTP 请求,并通过模块化架构加载扩展功能。在传统 LAMP 中,它加载 mod_php 模块来执行 PHP 代码。
    • PHP:作为服务器端脚本解释器,以 Apache 模块 (mod_php) 或 FastCGI 进程管理器 (php-fpm) 形式运行,负责解析并执行后端业务逻辑。
    • MySQL/MariaDB:作为关系型数据库管理系统(RDBMS),作为独立的守护进程运行,监听特定端口(默认 3306),提供数据的持久化存储、检索和事务处理。

    PHP 脚本通过数据库扩展(如 mysqli 或 PDO)通过 TCP 协议与 MySQL 进程通信。


    2. 软件包管理机制(“模块化”处理)

    在 Linux 发行版(如 Rocky Linux)中,采用包管理器(如 yum 或 dnf)管理依赖:

    • 主服务(如 httpd)是一个核心包。
    • 扩展/模块由独立的软件包提供,例如 php(PHP 解释器)、php-mysqlnd(连接 MySQL 的驱动)、mod_php(Apache 模块)。
    • 安装这些扩展包后,包管理器会自动在服务配置目录(如 /etc/httpd/conf.d/)中生成 .conf 配置文件。
    • Web 服务器启动时,会读取这些配置文件,动态加载所需的共享对象(.so 文件)或启动对应的进程。

    3. 前后端架构界定(“后端”的实质)

    整个 Web 系统遵循 C/S(客户端/服务器)架构:

    • 前端(客户端):主要是指用户的 Web 浏览器(如 Chrome),负责向服务器发送 HTTP 请求,并接收、解析、渲染服务器返回的 HTML、CSS、JavaScript 资源。
    • 后端(服务端):指运行在物理或虚拟服务器上的全部服务和进程,包括:
      • Web 服务器进程 (Apache):负责 HTTP 协议的请求响应。
      • 逻辑处理进程 (PHP):负责动态生成响应内容。
      • 数据库进程 (MySQL):负责数据的存储与读取。 这些后端进程运行在服务器端,不直接暴露给终端用户,而是通过网络协议彼此交互,最终向客户端返回处理后的结果。

    4. 扩展机制:安装包即添加模块

    无论是 yum 还是 apt 安装软件包,本质上是向特定的服务(Apache 或 Nginx)注入对应的功能模块或独立的处理进程。

    • 数据库是独立的模块:MySQL 安装后是一个独立的守护进程(服务),拥有独立的进程空间、端口(3306)和文件系统。
    • PHP 也是独立的模块:PHP 通常以 php-fpm(独立进程管理器)或 mod_php(嵌入 Apache 的模块)形式运行,专门负责解析 .php 脚本。

    5. 静态服务器 vs 动态服务器

    • 静态服务器:Apache 和 Nginx 默认是“静态 Web 服务器”。负责接收 HTTP 请求,并将硬盘上预先写好的 .html 或 .jpg 等静态文件原封不动地发送给浏览器。
    • 动态扩展:为了返回根据用户情况变化的页面,管理员挂载了 PHP 处理能力。当请求对象是 .php 文件时,Apache 或 Nginx 会将解析任务转交给 PHP。

    6. PHP、数据库与“动态返回”的完整工作流

    1. 触发(请求到达):用户在浏览器访问 http://ip/mysql.php,Apache 收到请求,识别为 .php 文件。
    2. 执行(PHP 处理):Apache 将执行权交给 PHP。PHP 解释器运行代码。
    3. 查询(连接数据库):PHP 代码调用 mysqli_connect() 等函数。PHP 进程通过 TCP 协议(例如 TCP 127.0.0.1:3306)向本机的 MySQL 守护进程发送 SQL 查询语句(如 SELECT * FROM users)。
    4. 获取(数据库返回):MySQL 接收到 SQL 指令,在磁盘或内存中查找数据,将结果集以二进制流的形式返回给 PHP 进程。
    5. 生成(PHP 组装):PHP 接收到数据库返回的数据(数组或对象),利用它生成动态的 HTML 字符串(例如 echo "连接MySQL...成功!";)。
    6. 返回(响应客户端):PHP 将生成的 HTML 完整字符串交还给 Apache,Apache 将其封装成 HTTP 响应包,通过网络发回给用户的浏览器。

    技术层面总结:Apache 负责 HTTP 协议通信,PHP 负责业务逻辑解析,MySQL 负责数据持久化存储与检索。三者作为独立的进程,通过 TCP 协议互相连接,实现了数据的动态存取和展示。