2023年3月

作者:vivo官网商城开发团队 - Xu Yi、Yan Chao

本文是vivo商城系列文章,主要介绍vivo商城库存系统发展历程、架构设计思路以及应对业务场景的实践。

一、业务背景

库存系统是电商商品管理的核心系统,本文主要介绍vivo商城库存中心发展历程、架构设计思路及应对各种业务场景的实践。

vivo商城原库存系统耦合在商品系统,考虑到相关业务逻辑复杂度越来越高,库存做了服务拆分,在可售库存管理的基础上新增了实物库存管理、秒杀库存、物流时效 、发货限制、分仓管理等功能,满足了商城库存相关业务需求。

本文将介绍vivo商城库存系统架构设计经验以及一些问题的解决方案。

二、系统架构设计

2.1 vivo大电商库存架构

根据vivo大电商的销售渠道与业务场景可以将库存业务架构分为3个层级:仓库层、调度层以及销售层。

仓库层对应实体仓库,包括自营仓库、顺丰仓等第三方仓库以及WMS系统、ERP系统等;调度层负责库存调度与订单发货管理;销售层包含多个服务终端,vivo官方商城、vivo门店、第三方电商分销渠道等。其分层结构如图所示:

图片

本文探讨的vivo官方商城库存架构设计,从整个vivo大电商库存架构来看,vivo官方商城库存系统涉及销售层内部架构以及销售层与调度层的交互。

2.2 商城库存系统架构演变

早期商城的库存冗余在各业务系统中,如可售库存在商品系统、活动库存在营销系统等,库存流转也只有扣减与释放,无法针对库存进行整合与业务创新,存在诸多限制:

  • 不能进行精细化管理,库存未分层,无法针对实物库存、分仓策略、活动库存进行精细化管理。

  • 没有分仓策略,无法提前获取商品收发地址,物流时效无法估算。

  • 无法针对地区、商品等进行发货管控。

  • 实时性差,无法及时同步实物库存以及分仓策略。

  • 性能弱,与其他系统耦合大,不能灵活扩展。

基于上述限制与产品期望,21年库存系统完成初版架构设计,此后系统不断迭代完善,形成当前的系统架构:

图片

库存系统提供两个核心能力:交易能力和库存管理。上层业务方可以调用提供的API完成库存查询、库存扣减等操作;管理台可以按成分仓策略、库存同步等操作。

三、系统业务架构

3.1 库存类型&分仓管理

3.1.1 库存类型结构

库存系统一共包含4类库存:可售库存、实物库存、预占库存、活动库存。

  • 可售库存:
    运营配置的普通商品库存,商品维度到SKU。

  • 实物库存:
    由仓储系统同步到库存系统的实物库存,细化到具体仓库。

  • 预占库存:
    用户下单完成库存预占,仓储系统发货后释放预占库存,预占库存可以监控已下单未发货库存量。

  • 活动库存:
    用于秒杀、抢购等各类营销活动的商品库存。

基于不同类型库存,可以构建一个简单的库存分层体系:

3.1.2 分仓管理

库存中心还维护了仓库信息、分仓策略、仓库实物库存信息等等:

  • 仓库信息:
    仓库基础信息,包括仓库地址、类型、编码等。

  • 分仓策略:
    仓库功能信息,仓库可发货区域、无实物库存后的备选仓库;订单根据收货地址对应优先发货的仓库,争取尽快发货尽早到货。

  • 仓库库存:
    仓库实物库存,由仓库调度系统同步到商城库存系统。

3.2 商城库存流转方案

商品库存流转涉及两个主要操作:正向库存扣减、逆向库存回退,整套库存变更流程如下:

图片

3.2.1 正向库存扣减流程

对于库存扣减,目前常见有两种库存扣减方案:

(1)下单时扣库存。

  • 优点是:
    实时扣库存,避免付款时因库存不足而阻断影响用户体验。

  • 缺点是:
    库存有限的情况下,恶意下单占库存影响其他正常用户下单。比如说有100台手机,如果没有限制下单数量,这100个库存可能被一个用户恶意占用,导致其他用户无法购买。

(2)支付时扣库存。

  • 优点是:
    不受恶意下单影响。

  • 缺点是:
    当支付订单数大于实际库存,会阻断部分用户支付,影响购物体验。比如说只有100台手机,但可能下了1000个订单,但有900个订单在支付时无法购买。

从用户体验考虑,我们采用的是下单时扣库存 + 回退这种方案。

下单时扣减库存,但只保留一段时间(比如15分钟),保留时间段内未支付则释放库存,避免长时间占用库存。

3.2.2 逆向库存回退流程

库存回退基于库存变更日志逐个回退。

库存回退基本流程:订单出库前用户申请退款,回退可售库存、回退预占库存、软删除扣减日志、增加回退日志;一旦商品出库,用户申请退货走处理机流程,可售库存和实物库存均不回退。

图片

3.3 精细化发货管控

库存系统还提供了一系列定制辅助功能:分仓策略、发货限制、物流时效等等。

(1)分仓策略

为了给用户更快的发货,我们采用的是分仓策略,即由最近的仓库(存在优先级)给用户发货;同时存在备选仓库,当所有仓库无实物库存时可走备选仓库。

3.3.1 发货限制

发货限制分地区限制时间限制。

  • 地区限制:
    根据收货地址批量设置部分区域无法发货等规则,粒度到省市区维度。

  • 时间限制:
    仓库的发货时效管理,包括每天的发货时段、大促发货时段、以及特殊情况下的停发时段。

3.3.2 物流时效预估

根据用户收货地址,基于分仓策略确定发货地址,再基于发货时效确定发货时间,提升用户体验。

四、系统架构技术要点

4.1 库存扣减防重

订单重复提交会导致库存重复扣减,比如用户误提交、系统超时重试等,针对此类问题有如下常见解决方案:

  1. 订单提交按钮单击置灰,避免重复提交。注:对于按钮置灰这种方案,可以减少用户误触重复提交的可能性,但不能从根本上解决库存被重复扣减的问题,比如通过脚本来刷扣减库存的接口,依旧造成库存的重复扣减。
  2. 保证库存扣减接口的幂等性。注:保证接口幂等的方案有很多,比如每次扣减库存时,带上唯一的流水号,利用数据库的唯一索引保证幂等等。
  3. 采用令牌机制。用户提交订单会进行令牌校验,校验通过才能提交订单。注:这种方案保证每次提交的订单是唯一的,如果用户多次下单,那么会产生多个订单。

本系统采用的是保证接口幂等性的方案。

在库存扣减接口入参中增加订单序列号作为唯一标识,库存扣减时增加一条扣减日志。当接口重复请求时,会优先校验是否已经存在扣减记录,如果已存在则直接返回,避免重复扣减问题,具体流程如下:

4.2 防超卖与高并发扣减方案

4.2.1 常规渠道防超卖方案

常规下单渠道流量小且对超卖风险厌恶度极高,常用的防超卖方案有:

方案一:
直接数据库扣减。通过sql判断剩余库存是否大于等于待扣库存,满足则扣减库存。该方案利用乐观锁原理即update的排他性确保事务性,避免超卖。

伪代码sql:

sql:update store set store = store - #{deductStore } where (store-#{deductStore }) >= 0 

该方案的
优点
是:

  • 实库实扣,不会出现超卖;

  • 数据库乐观锁保证并发扣减一致性;

  • 数据库事务保证批量扣减正常回滚。

该方案的
缺点
是:

  • 行级锁的原因存在性能瓶颈,高并发会出现请求堵塞超时问题;

  • 直连数据库,每次扣库存都是写操作,接口性能较低。

方案二:
利用分布式锁,强制串行化扣减同一商品库存。

该方案的
优点
是:

减轻数据库压力,同时还能确保不会超卖。

该方案的
缺点
是:

每次只能有一个请求抢占锁,不能应对高并发场景。

对于常规渠道,库存扣减是后置逻辑,流量不高,我们采用的是直接数据库扣减,且针对弊端做了一些
措施

  • 前置校验严格,同时针对刷单场景会有严格限流,保证最终扣减库存的流量可控;

  • 库存系统读写分离,减少数据库的压力。

4.2.2 高并发库存扣减方案

针对高并发库存扣减,比如秒杀,一般采用的是缓存扣减库存的方式(redis+lua脚本实现单线程库存更新)作为前置流程,代替数据库直接更新。

在redis中扣减库存虽然性能高,可以大大减轻数据库压力,但需要保证缓存数据能完整、正确的入库,以保证最终一致性。

针对缓存数据更新至数据库,目前主流方案有两种:

方案一:Redis数据直接异步更新至数据库。

优点
:简单、没有复杂的流程。

缺陷:
redis宕机或者故障,可能会造成缓存内库存数据的丢失。

方案二:Redis扣减库存时,同步在业务数据中insert库存信息。

这里大家可能会有两个疑问:

  1. 有数据库的插入操作,性能怎么保证?
  2. 有数据库的操作,又有redis的更新,事务性怎么保证?
  3. 异步更新业务库存在延迟,库存逆向回退如何保证?

对于疑问1:
由于数据库insert比update性能优,insert是在表的末尾直接插入,没有寻址的过程,可以保证性能比较快。

对于疑问2:
方案2不同于缓存直接扣减,而是把缓存扣减放在数据库insert的事务内,通过数据库的事务保证整体的事务。

insert的表被称为库存任务表,其中保存了库存扣减的信息,库存任务表结构可以设计的非常简单,主键 + 库存信息(json字符串)就可以了。

后续通过异步任务,从库存任务表表中查询出库存更新信息,将其同步到具体的库存表中,实现最终一致性,这种方案可以避免数据的丢失。

对于疑问3:
库存回退是根据业务库中扣减记录进行回退的,由于异步更新业务库必定存在延迟(延迟极低,数秒以内),所以极端场景会存在走退款逆向流程时业务库的库存扣减记录还未更新。

针对这种情况库存回退设置延迟重试机制,如果再极端点达到重试阈值依旧没有扣减记录,则返回回退成功,不做阻断。

目前我们针对秒杀库存扣减,采用的是方案2。但毕竟涉及数据库的更新,为了避免风险,在前置流量校验上做了限制,保证流量的可控:

4.2.3 库存热点问题

什么是热点问题?热点问题就是因热点商品导致的redis、数据库等性能瓶颈。在库存系统中,
热点问题主要存在

  • 采用
    直接扣减库存数据库
    的方式,存在数据库的行锁问题。常规渠道的库存扣减,我们采用的就是的就是这种方式。

  • 采用
    缓存扣减库存
    的方式,大流量的情况下,热点商品扣减库存操作会打向redis单片,造成单片性能抖动,从而出现redis性能瓶颈。

对于第1种热点问题,在vivo商城常见的场景是:新发的爆品手机,在准点售卖时会有抢购效应,容易造成库存数据库单行的瓶颈问题。针对这种热点问题,我们的解决方案是“分而治之”:

图片

对于潜在的热点爆款手机,我们会将库存平均分为多行(比如M行),扣减库存时,随机在M行中选取一行库存数据进行扣减。该方案突破了数据库单行锁的瓶颈限制,解决了爆款商品的热点问题。

对于第2种redis单片热点问题,解决方案也是分而治之。将数据库中的库存数据同步到redis时,把key值打散,分散在多个redis单片中。注:我们目前线上的流量峰值还达不到会造成redis单片瓶颈的问题,为避免过度设计,只做了前置限流,没有进行key值的打散。

4.3 库存同步方案

库存系统存在一些库存同步场景:

  • 对接仓储系统,完成实物库存同步。

  • 兼容历史架构,商品系统库存的可售库存同步等。

(1)实物库存同步:

实物库存同步,对接的是仓储系统,通过接口来获取商品的实际库存。实物库存同步分成两种:定时全量同步、指定单品更新。

  • 定时全量同步:
    每天定时全量拉取库存调度平台的实物库存进行全量同步。

  • 制定单品:
    运营也可以手动触发单个sku的商品即时同步实物库存。

(2)商品系统库存同步:

由于库存系统多个场景涉及库存变更,运营手动编辑、用户下单退款导致库存扣减回退,还有商品系统内编辑库存数据也会导致库存变更(以前库存系统未独立,库存数据维护在商品系统)。同时很多业务在查询库存时,参考的依旧是商品系统的库存数据。

这里有一个问题:库存系统已经独立出来,为什么还会依赖商品系统的库存数据?

这有两点
原因

  • 商城多个业务的后台有商品筛选的需要,商品筛选会有库存数量的筛选项。商品数量很多,筛选是分页的,如果将库存数据全部替换成库存系统的,那么存在跨系统分页问题,分页筛选会存在问题;

  • 历史遗留问题,很多业务方依赖的是商品系统的库存数据(包括依赖商品库存离线表的业务方),全部切换到库存系统,成本和影响范围大。

因此,我们需要保证商品系统和库存系统两边库存数据的一致。

库存变更场景多,为了降低业务复杂度、采用简单的方式实现库存同步,我们利用了团队自研的CDC系统(鲁班平台),整体流程如下:

图片

库存数据库发生变更后,鲁班平台通过binlog采集获取库存变更日志,再通过自定义规则筛选,然后发送mq变更消息,最后商品系统消费消息完成库存同步变更。

五、总结及展望

最后对库存系统进行一个
总结

库存系统完成服务拆分,在单一的可售库存扣减功能基础上拓展了很多功能,赋能业务的发展。

完成库存架构分层,抽象多个库存类型,更灵活地满足当前业务需求。

针对库存扣减防重、高并发场景下的库存扣减、库存热点问题、库存同步等技术问题,我们根据业务实际情况设计合理方案。

展望

目前vivo商城库存系统平台化能力不足,部分能力分散在其他系统中,未来我们希望能为vivo新零售提供一体化的库存管理方案。

3. 配置提供程序

上面提到,通过 IConfigurationBuilder 的实现类对象,我们可以自由地往配置系统中添加不同的配置提供程序,从而获取不同来源的配置信息。.NET Core 中,微软提供了以下这些内置的配置提供程序:

  • 文件配置提供程序
  • 环境变量配置提供程序
  • 命令行配置提供程序
  • Azure应用配置提供程序
  • Azure Key Vault 配置提供程序
  • Key-per-file配置提供程序
  • 内存配置提供程序
  • 应用机密(机密管理器)
  • 自定义配置提供程序

这里稍微介绍一下常用的几个。

3.1 文件配置提供程序

顾名思义,这个就是我们熟悉的配置加载方式,从配置文件中加载配置信息。配置文件多种多样,.NET Core框架内置支持Json、xml、ini三种格式的文件提供程序:

  • JSON配置提供程序(JsonConfigurationProvider)
  • XML配置提供程序(XmlConfigurationProvider)
  • INI配置提供程序(IniConfigurationProvider)

以上这些配置提供程序,均继承于抽象基类 FileConfigurationProvider,当一个提供程序中发现重复的键时,提供程序会引发 FormatException,所有类型的文件提供程序都是这样的机制。

另外,所有文件配置提供程序都支持提供两个配置参数:

  • optional:bool类型,指示该文件是否是可选的。如果该参数为false,但是指定的文件又不存在,则会报错。
  • reloadOnChange:bool类型,指示该文件发生更改时,是否要重新加载配置。

3.1.1 JSON配置提供程序

JSON配置提供程序被封装在
Microsoft.Extensions.Configuration.Json
Nuget包中,若通过 ConfigurationBuilder 自行构建配置系统需要先安装该依赖包。它通过 JsonConfigurationProvider 在运行时从 Json 文件中加载配置。

使用方式非常简单,通过 IConfigurationBuilder 的实现类对象调用 AddJsonFile 扩展方法指定Json配置文件的路径即可。以下代码可用于控制台程序中创建主机并设置配置系统:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

using var host = Host.CreateDefaultBuilder(args)
    .ConfigureAppConfiguration((context, config) =>
    {
        // 清除原有的配置提供程序
        config.Sources.Clear();

        var env = context.HostingEnvironment;
        // 添加 json 配置文件
        config.AddJsonFile("appsettings.json",true, true)
            .AddJsonFile($"appsetting.{env.EnvironmentName}.json", true, true);
    })
    .Build();

var configuration = host.Services.GetService<IConfiguration>();

Console.WriteLine($"Settings:Provider: {configuration.GetValue<string>("Settings:Provider")}");

host.Run();

appsetting.json 配置文件中的内容如下:

{
  "Settings": {
    "Provider": "JsonProvider",
    "version": {
      "subKey1": "value",
      "subKey2": 1
    },
    "items": [ "item1", "item2", "item3" ]
  }
}

控制台程序运行之后输出如下:

image

这样有一点要注意的是,对于我们手动添加的配置文件需要设置一下文件属性,让其在项目生成的时候能够正常生成到运行目录,确保应用可以正常获取到该文件:

image

3.1.2 XML配置提供程序

XML配置提供程序被封装在
Microsoft.Extensions.Configuration.Xml
Nuget包中,通过 XmlConfigurationProvider 类在运行时从 XML 文件加载配置。

使用方式也很简单,与 JSON 配置提供程序类似,通过 AddXmlFile 扩展方法指定配置文件路径。

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

using var host = Host.CreateDefaultBuilder(args)
    .ConfigureAppConfiguration((context, config) =>
    {
        // 清除原有的配置提供程序
        config.Sources.Clear();

        var env = context.HostingEnvironment;
        //// 添加 json 配置文件
        //config.AddJsonFile("appsettings.json",true, true)
        //    .AddJsonFile($"appsetting.{env.EnvironmentName}.json", true, true);

        config.AddXmlFile("appsettings.xml", true, true);
    })
    .Build();

var configuration = host.Services.GetService<IConfiguration>();

Console.WriteLine($"Settings:Provider: {configuration.GetValue<string>("Settings:Provider")}");
Console.WriteLine($"Settings:items[1]: {configuration.GetValue<string>("Settings:items:1")}");

host.Run();

xml配置文件内容如下:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
	<Settings>
		<Provider>XmlProvider</Provider>
		<version>
			<subKey1>value</subKey1>
			<subKey2>1</subKey2>
		</version>
		<items>item1</items>
		<items>item2</items>
		<items>item3</items>
	</Settings>
</configuration>

运行程序控制台输出如下:

image

这里有一个和版本有关的点,对Xml文件中使用同一元素名称的重复元素,一般也就是数组,.NET 6及之后的xml配置提供程序会自动为其编制索引,不再需要显式指定name属性。如果是 .NET 6 以下的版本则需要这样了:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
	<Settings>
		<Provider>XmlProvider</Provider>
		<version>
			<subKey1>value</subKey1>
			<subKey2>1</subKey2>
		</version>
		<items name="itemkey1">item1</items>
		<items name="itemkey2">item2</items>
		<items name="itemkey3">item3</items>
	</Settings>
</configuration>
Console.WriteLine($"Settings:items[1]: {configuration.GetValue<string>("Settings:items:itemkey2")}");

另外xml文件中的属性也可用于提供值:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <key attribute="value" />
  <section>
    <key attribute="value" />
  </section>
</configuration>

获取属性的值可用以下配置键:

key:attribute
section:key:attribute

3.1.3 INI配置提供程序

INI 配置提供程序被封装在
Microsoft.Extensions.Configuration.Ini
Nuget包,通过 IniConfigurationProvider 类在运行时从 INI 文件加载配置。使用方式如下:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

using var host = Host.CreateDefaultBuilder(args)
    .ConfigureAppConfiguration((context, config) =>
    {
        // 清除原有的配置提供程序
        config.Sources.Clear();

        var env = context.HostingEnvironment;
        //// 添加 json 配置文件
        //config.AddJsonFile("appsettings.json",true, true)
        //    .AddJsonFile($"appsetting.{env.EnvironmentName}.json", true, true);

        //config.AddXmlFile("appsettings.xml", true, true);

        config.AddIniFile("appsettings.ini", true, true);
    })
    .Build();

var configuration = host.Services.GetService<IConfiguration>();

Console.WriteLine($"Settings:Provider: {configuration.GetValue<string>("Settings:Provider")}");
Console.WriteLine($"Settings:items[1]: {configuration.GetValue<string>("Settings:items:1")}");

host.Run();

ini配置文件内容如下:

[Settings]
Provider="IniProvider"
version:subKey1="value"
version:subKey2=1
items:0="item1"
items:1="item2"
items:3="item3"

运行应用,控制台输出如下:
image

3.2 环境变量配置提供程序

环境变量配置提供程序被封装在 Microsoft.Extensions.Configuration.EnvironmentVariables, 通过 EnvironmentVariablesConfigurationProvider 在运行时从环境变量中以键值对的方式加载配置。

环境变量一般情况下是配置在机器上的,而不同的操作系统对环境变量的设置要求有所不同,当环境变量存在多层的时候,层级之间的分隔有些支持通过 : 号进行分隔,有些不支持,双下划线(
)是全平台支持的,所以设置环境变量的时候要使用双下划线(
)来代替冒号(:)。

各种不同的平台下怎么去添加环境变量这里就不细说了,Windows 下大家最起码都应该知道可以通过 我的电脑 -> 属性 -> 高级系统设置 去可视化的添加,命令行的方式可阅读下官方文章:
ASP.NET Core 中的配置 | Microsoft Learn
,Linux 平台下可以通过 export 命令临时添加,或者修改相应的配置文件 ~/.bashrc 或 /etc/profile,大家仔细查一下资料就行了。

处理在机器上直接设置环境变量外,我们开发测试的过程中也可以通过 ASP.NET Core框架下的 launchSettings.json 配置文件设置用于调试的临时环境变量。在应用启动调试时,该文件中的环境变量会替代系统的中的环境变量。

{
  "$schema": "https://json.schemastore.org/launchsettings.json",
  "profiles": {
    "ConfigurationSample": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "launchUrl": "swagger",
      "applicationUrl": "http://localhost:5004",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development",
        "Custom_settings__Provider": "EnvironmentVariablesProvider",
        "Custom_settings__version__subKey1": "value",
        "Custom_settings__items__0": "item1",
        "Custom_settings__items__1": "item2",
        "Custom_settings__items__2": "item3"
      }
    }
  }
}

环境变量配置提供程序使用也很简单,注意以下示例为了使用 launchSettings.json 中的环境变量是在 ASP.NET Core项目中测试的。

var builder = WebApplication.CreateBuilder(args);

builder.Host.ConfigureAppConfiguration(builder =>
{
    builder.Sources.Clear();
    // 筛选前置为 Custom_ 的环境变量,将其加载为应用配置,其他的不加载
    builder.AddEnvironmentVariables("Custom_");
});

var app = builder.Build();

Console.WriteLine($"Settings:Provider: {app.Configuration.GetValue<string>("Settings:Provider")}");
Console.WriteLine($"Settings:items[1]: {app.Configuration.GetValue<string>("Settings:items:1")}");

app.Run();

在添加环境变量时,通过指定参数 prefix,只读取限定前缀的环境变量。不过在读取环境变量时,会将前缀删除。如果不指定参数 prefix,那么会读取所有环境变量。

当创建默认通用主机(Host)时,默认就已经添加了前缀为
DOTNET_
的环境变量,如果是在 ASP.NET Core 中,配置了 Web主机时,默认添加了前缀为
ASPNETCORE_
的环境变量,而后主机加载应用配置时,再根据策略添加了其他的环境变量,如果没有传递 prefix 参数则是所有环境变量。这一块的加载机制,下面再细讲。

运行应用,控制台输出如下:
image

除此之外,环境变量提供程序还有一些隐藏的功能点,当没有向 AddEnvironmentVariables 传入前缀时,默认也会针对含有以下前缀的环境变量进行特殊处理:

image

这个功能点比较少用到,但是大家看到这个大概都会有点疑惑,具体的形式是怎么样的,下面稍微测试一下

首先在 launchSettings.json 文件中添加多一个环境变量:

"MYSQLCONNSTR_Default": "Server=myServerAddress;Database=myDataBase;Uid=myUsername;Pwd=myPassword;"

之后在应用中打印如下两个配置:

Console.WriteLine($"ConnectionStrings:Default: { app.Configuration.GetValue<string>("ConnectionStrings:Default") }");
Console.WriteLine($"ConnectionStrings:Default_Provider: { app.Configuration.GetValue<string>("ConnectionStrings:Default_ProviderName") }");

输出结果如下:
image

也就是说,这种形式的环境变量会被自动转换为两个。

3.3 命令行配置提供程序

命令行配置提供程序被封装在 Microsoft.Extensions.Configuration.CommandLine 包中,通过CommandLineConfigurationProvider在运行时从命令行参数键值对中加载配置。

当我们通过 dotnet 命令启动一个 .NET Core 应用时,我们可以在命令后面追加一些参数,这些参数将在入口文件中被 args 变量接收到。命令行配置提供程序使用如下:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

using var host = Host.CreateDefaultBuilder(args)
    .ConfigureAppConfiguration((context, config) =>
    {
        // 清除原有的配置提供程序
        config.Sources.Clear();
        config.AddCommandLine(args);
    })
    .Build();

var configuration = host.Services.GetService<IConfiguration>();

Console.WriteLine($"Settings:Provider: {configuration.GetValue<string>("Settings:Provider")}");
Console.WriteLine($"Settings:items[1]: {configuration.GetValue<string>("Settings:items:1")}");

host.Run();

之后通过命令行程序启动应用,并传入相应的参数:

dotnet ConfigurationSampleConsole.dll Settings:Provider=CommandLineProvider Settings:items:1=item1

image

命令行参数的设置有三种方式:

(1) 使用 = 号连接键值:

dotnet ConfigurationSampleConsole.dll Settings:Provider=CommandLineProvider Settings:items:0=item1 Settings:items:1=item2

(2) 使用 / 号表示键,值跟在键后面,键值以空格分隔

dotnet ConfigurationSampleConsole.dll /Settings:Provider CommandLineProvider /Settings:items:0 item1 /Settings:items:1 item2

(3) 使用 -- 符号表示键,值跟在键后面,键值以空格分隔

dotnet ConfigurationSampleConsole.dll --Settings:Provider CommandLineProvider --Settings:items:0 item1 --Settings:items:1 item2

如果值之中本来就有空格的,可以使用 "" 号包括。

dotnet ConfigurationSampleConsole.dll --Settings:Provider CommandLineProvider --Settings:items:0 item1 --Settings:items:1 "test item2"

AddCommandLine 扩展方法提供了重载,允许额外传入一个参数,该参数提供一个交换映射字典,针对命令行配置参数进行key映射。例如命令行传入键是 name01 ,映射后的的键为 project:name。这里有一些要注意的点:

  • 交换映射key必须以-或--开头。当使用-开头时,命令行参数书写时也要以-开头,当使用--开头时,命令行参数书写时可以以--或/开头。
  • 交换映射字典中的key不区分大小写,不能包含重复 key。如不能同时出现-n和-N,但可以同时出现 -n 和 --n 。
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

using var host = Host.CreateDefaultBuilder(args)
    .ConfigureAppConfiguration((context, config) =>
    {
        // 清除原有的配置提供程序
        config.Sources.Clear();

        var switchMappings = new Dictionary<string, string>
        {
            ["--b1"] = "Settings:Provider",
            ["-b2"] = "Settings:items"
        };

        config.AddCommandLine(args, switchMappings);
    })
    .Build();

var configuration = host.Services.GetService<IConfiguration>();

Console.WriteLine($"Settings:Provider: {configuration.GetValue<string>("Settings:Provider")}");
Console.WriteLine($"Settings:items[1]: {configuration.GetValue<string>("Settings:items:1")}");

host.Run();

image

3.4 内存配置提供程序

内存配置提供程序就比较简单了,它直接被包含在 Microsoft.Extensions.Configuration,通过MemoryConfigurationProvider在运行时从内存中的集合中加载配置。使用方式如下:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

using var host = Host.CreateDefaultBuilder(args)
    .ConfigureAppConfiguration((context, config) =>
    {
        // 清除原有的配置提供程序
        config.Sources.Clear();

        config.AddInMemoryCollection(new Dictionary<string, string> {
            { "Settings:Provider", "InMemoryProvider" },
            { "Settings:items:1", "MemoryItem" }
        });
    })
    .Build();

var configuration = host.Services.GetService<IConfiguration>();

Console.WriteLine($"Settings:Provider: {configuration.GetValue<string>("Settings:Provider")}");
Console.WriteLine($"Settings:items[1]: {configuration.GetValue<string>("Settings:items:1")}");

host.Run();![image]

image

3.5 配置加载顺序

上面介绍了一些常用的配置提供程序,这些配置提供程序都是通过扩展方法添加到 ConfigurationBuilder 对象中的,而从上面 ConfigurationBuilder 的源码可以看出,添加一个配置提供程序的时候其实应该是添加了一个对应的 IConfigurationSource 对象,而后在 ConfigurationBuilder 中被保存到集合中。

这就可以看出,配置系统是允许同时添加多种配置提供程序,支持多来源的配置信息同时存在的。那么当多个配置处理程序都被添加到配置系统之中,那我们从配置系统中通过配置键获取配置值的时候是怎么进行的呢,当多个配置提供程序存在相同的配置键时,我们获取到的配置值是哪个呢?

从 ConfigurationRoot 的源码中可以可以看到,当我们用索引器 API 读取配置值时,是调用了 GetConfiguration 方法

image

而 GetConfiguration 方法中的逻辑也很简单,只是遍历提供程序集合尝试从提供程序去获取值,需要关注的是遍历的顺序。

image

这里的逻辑是这样子的,倒叙进行遍历,后添加的配置处理程序先被遍历,一旦通过key从提供程序中获取到值就返回结果,不再继续遍历。所以添加配置提供程序的顺序决定相同配置键最终的值, 当多个配置处理程序存在相同键时,越后添加的配置提供程序优先级越高,从最后的一个提供程序获取到值之后就不再从其他处理程序获取。

3.6 默认配置来源

上面也有提到通过主机运行和管理应用,在通过主机运行的项目中,主机在启动的时候就已经默认添加了一些配置提供程序,所以我们创建了一个 ASP.NET Core 模板项目之后就可以获取到 appsettings.json 等配置文件中的配置信息。下面介绍一下默认添加的配置提供程序。


Host.CreateDefaultBuilder(String[])
方法或者 WebApplication.CreateBuilder(args) 方法执行的时候,会按照以下顺序添加应用的配置提供程序:

(1) 内存配置提供程序
(2)
Chained
配置提供程序(添加现有的主机配置)
(3)
JSON 配置提供程序
(添加 appsettings.json 配置文件)
(4)
JSON 配置提供程序
(添加 appsettings.{Environment}.json 配置文件)
(5) 机密管理器(仅Windows)
(6)
环境变量配置提供程序
(未限定前缀)
(7)
命令行配置提供程序

配置分主机配置和应用配置,主机启动时应用仍未启动,主机启动过程中的配置就是主机配置。上面第一个Chained 配置提供程序就是承接过来的主机配置。而主机配置是按照以下顺序加载的:

(1)
环境变量配置提供程序
(以 DOTNET_ 为前缀的环境变量)
(2)
命令行配置提供程序
(命令行参数)
(3)
环境变量配置提供程序
(以 ASPNETCORE_ 为前缀的环境变量,如果是Web主机的话)

所以最终的应用配置加载顺序应该是下面这样:

(1) 内存配置提供程序
(2)
环境变量配置提供程序
(以 DOTNET_ 为前缀的环境变量)
(3)
命令行配置提供程序
(命令行参数)
(4)
环境变量配置提供程序
(以 ASPNETCORE_ 为前缀的环境变量,如果是Web主机的话)
(5)
JSON 配置提供程序
(添加 appsettings.json 配置文件)
(6)
JSON 配置提供程序
(添加 appsettings.{Environment}.json 配置文件)
(7) 机密管理器(仅Windows)
(8)
环境变量配置提供程序
(未限定前缀)
(9)
命令行配置提供程序
(命令行参数)

按照越后面添加的提供程序优先的方式,最终应用配置会覆盖主机配置,并且最优先是最后添加的命令行配置提供程序,我们可以通过以下方式打印配置系统中所有的配置提供程序,进行验证:

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

var configurationRoot = (IConfigurationRoot)app.Configuration;
foreach (var provider in configurationRoot.Providers.AsEnumerable())
{
    Console.WriteLine(provider.ToString());
}

app.Run();

最终控制台打印出来的结果如下:

最终控制台打印出来的结果如下:
image

虽然应用配置优先,会覆盖前面的主机配置,但是有一些变量会在初始化主机生成器的时候就提前进行锁定,并且之后不会受应用配置的影响:

这里提到环境名称,其实也就是软件运行的环境,最最基本的也会分为开发环境、生产环境两种。软件运行环境通过环境变量来设置,普通的.NET Core 应用环境变量key为NETCORE_ENVIRONMENT,Web应用环境变量key为ASPNETCORE_ENVIRONMENT,Web应用下如果两者同时存在,后者会覆盖前者。软件应用根据不同的环境会有不同的行为逻辑,例如上面讲到的 appsettings.{environment}.json 根据环境而不同的配置文件,例如之前的 入口文件 文章中讲到的 Startup 文件根据不同环境的分离配置方式,而我们在代码中有时也会根据环境处理不同的逻辑,这时候我们可以注入 IHostEnvironment 服务,通过它获取当前应用的运行环境,入口文件中无论是 WebApplicationBuilder 对象还是 WebApplication 对象都包含该类型的属性。

通过环境变量设置当前运行环境,其实环境变量的值只是一个字符串,我们可以设置成任意值,这是运行的,.NET Core 框架下 IHostEnvironment 也能够正常加载到相应的环境名称,但是.NET Core 默认只提供了对 Development、Production 和 Staging 三种环境的判别,以及相应的处理逻辑和扩展方法,如果是其他的自定义环境则需要开发人员自行进行相应的处理了。和 .NET Core 应用环境相关的知识点大家可以看一下官方文档:
在 ASP.NET Core 中使用多个环境 | Microsoft Learn

除了上面讲到的主机配置,其他还有一些主机配置,例如URLS,但这个是可以通过应用配置设置的,读取相应的配置值时也应用从应用配置读取。

URLS 配置Web应用启动后的访问地址,这个配置可以多个地方设置,其中命令行参数最优先,其他地方设置的应该被命令行参数覆盖。但是如果通过Kestrel 终结点方式设置了Web应用的访问地址,那Kestrel 终结点的配置将覆盖其他所有的访问地址的配置。

如在 appsettings.json 中添加以下配置:

"Kestrel": {
    "Endpoints": {
      "Https": {
        "Url": "https://localhost:9999"
      }
    }
  }

那么以下几种方式设置的 URLS 都会失效:

  • UseUrls
  • 命令行上的 --urls
  • 环境变量 ASPNETCORE__URLS

也就是说,就算我们用以下命令启动应用,应用最终的访问地址还是以Kestrel终结点配置的为准:

dotnet run --urls="https://localhost:7777"

Kestrel 配置与 URLS 配置不是一个参数,我们可以通过在命令行或者环境变量中设置 kestrel 中间点配置来覆盖 appsettings.json 中的,这又回到配置提供程序的优先级问题了。

set Kestrel__Endpoints__Https__Url=https://localhost:8888

dotnet run Kestrel__Endpoints__Https__Url=https://localhost:8888

在主机启动的逻辑中Kestrel具备更高的最终优先级,但是其实主机内部是先根据URLS创建了一个终结点,之后又替换为 Kestrel 配置的终结点的。通过应用启动时的控制台输出可以看出。

image

这种情况对于单机应用没有什么影响,但是对于使用自动服务发现的微服务架构而言就可能有问题了,可能导致注册服务注册中心的终结点是第一个,而后应用终结点又被改变,导致注册中心记录的服务终结点和实际的不一致。

这一篇的内容比较多,但是不大好拆分,整个知识点是一个整体,通过这一章的内容相信大家能够对 .NET Core 框架配置系统内部的工作原理有一个详细的了解。



参考文章:

ASP.NET Core 中的配置 | Microsoft Learn
配置 - .NET | Microsoft Learn
理解ASP.NET Core - 配置(Configuration)



ASP.NET Core 系列:
目录:
ASP.NET Core 系列总结
上一篇:
ASP.NET Core - 配置系统之配置添加

从0搭建属于自己的服务器

最近小伙伴推荐的华为云活动,购买服务器相当的划算,本人也是耗费巨资购买了一台2核4G HECS云服务器。
话不多说,在这里给华为云打一个广子,活动力度还是很不错的。
活动详情见链接:
https://kuy8.com/xcGtU

1、购买与搭建


一般个人使用,我觉得2核4G的已经绰绰有余啦,所以本文也是基于这个配置来搭建的(看准最便宜的下手

  • 编译安装 HAProxy 新版 LTS 版本,编译安装 Keepalived
  • 开启HAProxy多线程,线程数与CPU核心数保持一致,并绑定CPU核心
  • 因业务较多避免配置文件误操作,需要按每业务一个配置文件并统一保存至/etc/haproxy/conf.d目录中
  • 基于ACL实现单IP多域名负载功能,两个域名的业务: www.yanlinux.org 和 www.yanlinux.edu
  • 实现MySQL主从复制
  • 对 www.yanlinux.edu 域名基于HAProxy+Nginx+Tomcat+MySQL,并实现Jpress的JAVA应用
  • 对 www.yanlinux.org 域名基于HAProxy+Nginx+PHP+MySQL+Redis,实现phpMyadmin的PHP应用,并实现Session会话保持统一保存到Redis

1 DNS服务器配置

在10.0.0.7主机上搭建www.yanlinux.org(VIP:10.0.0.100)和www.yanlinux.edu(VIP:10.0.0.200)的DNS解析。

配置的关键:

  • 在主配置文件
    /etc/named.conf
    中要将
    listen-on port 53 { 127.0.0.1; }
    中的
    127.0.0.1
    改为
    localhost
    ;还需要将
    allow-query { localhost; };
    前面加上
    //
    注释掉,或者将其中的
    localhost
    改为
    any
    ,或者在后面加上各个网段信息。
  • 各个域名解析库文件的权限应改为641,属组为
    named
#利用脚本自动搭建www.yanlinux.org的dns解析配置
[root@dns ~]$ cat install_dns.sh 
#!/bin/bash

DOMAIN=yanlinux.org
HOST=www
HOST_IP=10.0.0.100

CPUS=`lscpu |awk '/^CPU\(s\)/{print $2}'`
. /etc/os-release

color () {
    RES_COL=60
    MOVE_TO_COL="echo -en \\033[${RES_COL}G"
    SETCOLOR_SUCCESS="echo -en \\033[1;32m"
    SETCOLOR_FAILURE="echo -en \\033[1;31m"
    SETCOLOR_WARNING="echo -en \\033[1;33m"
    SETCOLOR_NORMAL="echo -en \E[0m"
    echo -n "$1" && $MOVE_TO_COL
    echo -n "["
    if [ $2 = "success" -o $2 = "0" ] ;then
        ${SETCOLOR_SUCCESS}
        echo -n $"  OK  "    
    elif [ $2 = "failure" -o $2 = "1"  ] ;then 
        ${SETCOLOR_FAILURE}
        echo -n $"FAILED"
    else
        ${SETCOLOR_WARNING}
        echo -n $"WARNING"
    fi
    ${SETCOLOR_NORMAL}
    echo -n "]"
    echo 
}


install_dns () {
    if [ $ID = 'centos' -o $ID = 'rocky' ];then
	    yum install -y  bind bind-utils
	elif [ $ID = 'ubuntu' ];then
        color "不支持Ubuntu操作系统,退出!" 1
        exit
	    #apt update
	    #apt install -y  bind9 bind9-utils
	else
	    color "不支持此操作系统,退出!" 1
	    exit
	fi
    
}

config_dns () {
    sed -i -e '/listen-on/s/127.0.0.1/localhost/' -e '/allow-query/s/localhost/any/' /etc/named.conf
    cat >> 	/etc/named.rfc1912.zones <<EOF
zone "$DOMAIN" IN {
    type master;
    file  "$DOMAIN.zone";
};
EOF
   cat > /var/named/$DOMAIN.zone <<EOF
\$TTL 1D
@	IN SOA	master admin.$DOMAIN (
					1	; serial
					1D	; refresh
					1H	; retry
					1W	; expire
					3H )	; minimum
	        NS	 master
master      A    `hostname -I`         
$HOST     	A    $HOST_IP
EOF
	#修改权限和属组
   chmod 640 /var/named/$DOMAIN.zone
   chgrp named /var/named/$DOMAIN.zone
}

start_service () {
    systemctl enable --now named
	systemctl is-active named.service
	if [ $? -eq 0 ] ;then 
        color "DNS 服务安装成功!" 0  
    else 
        color "DNS 服务安装失败!" 1
        exit 1
    fi   
}

install_dns
config_dns
start_service

[root@dns ~]$ sh install_dns.sh

#yanlinux.org.zone区域数据文件信息
[root@dns ~]$ cat /var/named/yanlinux.org.zone 
$TTL 1D
@	IN SOA	master admin.yanlinux.org (
					1	; serial
					1D	; refresh
					1H	; retry
					1W	; expire
					3H )	; minimum
	        NS	 master
master      A    10.0.0.7          
www     	A    10.0.0.100

#然后拷贝yanlinux.org.zone区域子配置文件创建yanlinux.edu.zone区域子配置文件.若是自己重新创建yanlinux.edu.zone子配置文件,创建完以后需要将子配置文件的文件权限改为640以及属组改为named
[root@dns ~]$ cd /var/named
[root@dns named]$ cp -a yanlinux.org.zone yanlinux.edu.zone
#修改yanlinux.edu对应的信息
[root@dns named]$ vi yanlinux.edu.zone
$TTL 1D
@	IN SOA	master admin.yanlinux.edu (
					1	; serial
					1D	; refresh
					1H	; retry
					1W	; expire
					3H )	; minimum
	        NS	 master
master      A    10.0.0.7
www     	A    10.0.0.200

#两个域名对应的子配置文件已经创建好,然后在/etc/named.rfc1912.zones中添加区域子配置文件的信息
[root@dns ~]$ vi /etc/named.rfc1912.zones
......
#添加一下信息
zone "yanlinux.org" IN {
    type master;
    file  "yanlinux.org.zone";
};
zone "yanlinux.edu" IN {
    type master;
    file  "yanlinux.edu.zone";
};

#重新加载配置信息
[root@dns ~]$ rndc reload
server reload successful


[root@dns ~]$ dig www.yanlinux.org

; <<>> DiG 9.11.4-P2-RedHat-9.11.4-26.P2.el7 <<>> www.yanlinux.org
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 56759
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 2

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; MBZ: 0x0005, udp: 4096
;; QUESTION SECTION:
;www.yanlinux.org.		IN	A

;; ANSWER SECTION:
www.yanlinux.org.	5	IN	A	10.0.0.100

;; AUTHORITY SECTION:
yanlinux.org.		5	IN	NS	master.yanlinux.org.

;; ADDITIONAL SECTION:
master.yanlinux.org.	5	IN	A	10.0.0.7

;; Query time: 0 msec
;; SERVER: 10.0.0.2#53(10.0.0.2)
;; WHEN: Wed Mar 08 21:48:00 CST 2023
;; MSG SIZE  rcvd: 98

[root@dns ~]$ dig www.yanlinux.edu

; <<>> DiG 9.11.4-P2-RedHat-9.11.4-26.P2.el7 <<>> www.yanlinux.edu
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 19598
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 2

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; MBZ: 0x0005, udp: 4096
;; QUESTION SECTION:
;www.yanlinux.edu.		IN	A

;; ANSWER SECTION:
www.yanlinux.edu.	5	IN	A	10.0.0.200

;; AUTHORITY SECTION:
yanlinux.edu.		5	IN	NS	master.yanlinux.edu.

;; ADDITIONAL SECTION:
master.yanlinux.edu.	5	IN	A	10.0.0.7

;; Query time: 0 msec
;; SERVER: 10.0.0.2#53(10.0.0.2)
;; WHEN: Wed Mar 08 21:48:06 CST 2023
;; MSG SIZE  rcvd: 98

2 客户端配置

在10.0.0.17主机上,设置DNS服务器的IP作为其DNS,做这一步之前一定要在搭建DNS服务器时,
做好上面提到的第一个关键点,不然客户端不能正确解析到域名

[root@internet ~]$ cat /etc/sysconfig/network-scripts/ifcfg-eth0 
BOOTPROTO="static"
NAME="eth0"
DEVICE="eth0"
IPADDR=10.0.0.17
PREFIX=24
GATEWAY=10.0.0.2
DNS1=10.0.0.7  #改成DNS服务器的IP
#DNS2=114.114.114.114
ONBOOT="yes"

#重启网络服务
[root@internet ~]$ systemctl restart network
[root@internet network-scripts]$ cat /etc/resolv.conf 
# Generated by NetworkManager
nameserver 10.0.0.7

#测试解析
[root@internet ~]$ host www.baidu.com
www.baidu.com is an alias for www.a.shifen.com.
www.a.shifen.com has address 36.152.44.95
www.a.shifen.com has address 36.152.44.96

[root@internet ~]$ dig www.yanlinux.org

; <<>> DiG 9.11.4-P2-RedHat-9.11.4-26.P2.el7 <<>> www.yanlinux.org
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 19011
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 2

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;www.yanlinux.org.		IN	A

;; ANSWER SECTION:
www.yanlinux.org.	86400	IN	A	10.0.0.100

;; AUTHORITY SECTION:
yanlinux.org.		86400	IN	NS	master.yanlinux.org.

;; ADDITIONAL SECTION:
master.yanlinux.org.	86400	IN	A	10.0.0.7

;; Query time: 0 msec
;; SERVER: 10.0.0.7#53(10.0.0.7)
;; WHEN: Thu Mar 09 10:40:06 CST 2023
;; MSG SIZE  rcvd: 98

[root@internet ~]$ dig www.yanlinux.edu

; <<>> DiG 9.11.4-P2-RedHat-9.11.4-26.P2.el7 <<>> www.yanlinux.edu
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 64928
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 2

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;www.yanlinux.edu.		IN	A

;; ANSWER SECTION:
www.yanlinux.edu.	86400	IN	A	10.0.0.200

;; AUTHORITY SECTION:
yanlinux.edu.		86400	IN	NS	master.yanlinux.edu.

;; ADDITIONAL SECTION:
master.yanlinux.edu.	86400	IN	A	10.0.0.7

;; Query time: 0 msec
;; SERVER: 10.0.0.7#53(10.0.0.7)
;; WHEN: Thu Mar 09 10:40:11 CST 2023
;; MSG SIZE  rcvd: 98

3 部署NFS主备服务

  1. 搭建主NFS服务器
[root@NFS ~]$ yum -y install nfs-utils
[root@NFS ~]$ systemctl enable --now nfs-server.service

#创建用于传输的用户
[root@NFS ~]$ groupadd -g 666 www 
[root@NFS ~]$ useradd -u 666 www -g 666

#创建NFS共享文件夹
[root@NFS ~]$ mkdir /data/www -p
[root@NFS ~]$ chown -R www. /data/www/

[root@NFS ~]$ mkdir /data/web2
[root@NFS ~]$ chown -R www.www /data/web2/

#添加共享配置
[root@NFS ~]$ vi /etc/exports
/data/www *(rw,all_squash,anonuid=666,anongid=666) #具有读写权限,所有远程用户映射为666对应的用户
/data/web2 *(rw,all_squash,anonuid=666,anongid=666)

#重启
[root@NFS ~]$ systemctl restart nfs-server.service
[root@NFS ~]$ showmount -e 10.0.0.68
Export list for 10.0.0.68:
/data/web2 *
/data/www  *


#下载sersync,实现数据实时备份同步到NFS备份服务器
#下载sersync,解压,设置PATH变量
[root@NFS ~]$ wget https://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com/sersync/sersync2.5.4_64bit_binary_stable_final.tar.gz
[root@NFS ~]$ tar xf sersync2.5.4_64bit_binary_stable_final.tar.gz
[root@NFS ~]$ cp -a GNU-Linux-x86/ /usr/local/sersync
[root@NFS ~]$ echo "PATH=/usr/local/sersync:$PATH" > /etc/profile.d/sersync.sh
[root@NFS ~]$ source /etc/profile.d/sersync.sh

#生成验证文件
[root@NFS ~]$ echo lgq123456 > /etc/rsync.pas
[root@NFS ~]$ chmod 600 /etc/rsync.pas

#备份sersync配置文件,修改配置文件
[root@NFS ~]$ cp -a /usr/local/sersync/confxml.xml{,.bak}
##web1(可道云)共享业务配置
[root@NFS ~]$ vi /usr/local/sersync/confxml.xml
1 <?xml version="1.0" encoding="ISO-8859-1"?>
  2 <head version="2.5">
  3     <host hostip="localhost" port="8008"></host>
  4     <debug start="false"/>
  5     <fileSystem xfs="false"/>
  6     <filter start="false">
  7     <exclude expression="(.*)\.svn"></exclude>
  8     <exclude expression="(.*)\.gz"></exclude>
  9     <exclude expression="^info/*"></exclude>
 10     <exclude expression="^static/*"></exclude>
 11     </filter>
 12     <inotify>
 13     <delete start="true"/>
 14     <createFolder start="true"/>
 15     <createFile start="false"/>
 16     <closeWrite start="true"/>
 17     <moveFrom start="true"/>
 18     <moveTo start="true"/>
 19     <attrib start="true"/> ##修改此行为true,文件属性变化后也会同步
 20     <modify start="false"/>
 21     </inotify>
 22 
 23     <sersync>
 24     <localpath watch="/data/www"> ##修改此行,需要同步的源目录
 25         <remote ip="10.0.0.48" name="backup"/> #修改此行,指定备份服务器地址和rsync daemon的模块名,开启了ssh start,此时name为远程的shell方式运行时的目标目录
 26         <!--<remote ip="192.168.8.39" name="tongbu"/>-->
 27         <!--<remote ip="192.168.8.40" name="tongbu"/>-->
 28     </localpath>
 29     <rsync>
 30         <commonParams params="-artuz"/>
 31         <auth start="true" users="rsyncuser" passwordfile="/etc/rsync.pas"/> #修改此行为true,指定备份服务器的rsync配置的用户和密码
 ......
 
 #以后台方式执行同步
 [root@NFS ~]$ sersync2 -dro /usr/local/sersync/confxml.xml
 
 ##web2(jpress)业务共享配置
 [root@NFS ~]$ cd /usr/local/sersync/
 root@NFS sersync]$ cp confxml.xml jpress.xml
 ###相较于web1只需修改下面标记的两处
 [root@NFS sersync]$ vi jpress.xml
 <?xml version="1.0" encoding="ISO-8859-1"?>
  2 <head version="2.5">
  3     <host hostip="localhost" port="8008"></host>
  4     <debug start="false"/>
  5     <fileSystem xfs="false"/>
  6     <filter start="false">
  7     <exclude expression="(.*)\.svn"></exclude>
  8     <exclude expression="(.*)\.gz"></exclude>
  9     <exclude expression="^info/*"></exclude>
 10     <exclude expression="^static/*"></exclude>
 11     </filter>
 12     <inotify>
 13     <delete start="true"/>
 14     <createFolder start="true"/>
 15     <createFile start="false"/>
 16     <closeWrite start="true"/>
 17     <moveFrom start="true"/>
 18     <moveTo start="true"/>
 19     <attrib start="true"/>
 20     <modify start="false"/>
 21     </inotify>
 22 
 23     <sersync>
 24     <localpath watch="/data/web2">  #只需要将web1中的这个共享目录改成web2的
 25         <remote ip="10.0.0.78" name="web2-backup"/> #这个是备份服务器中定义对应web2的rsync daemon的模块名
 26         <!--<remote ip="192.168.8.39" name="tongbu"/>-->
 27         <!--<remote ip="192.168.8.40" name="tongbu"/>-->
 28     </localpath>
 29     <rsync>
 30         <commonParams params="-artuz"/>
 31         <auth start="true" users="rsyncuser" passwordfile="/etc/rsync.pas"/>
 32         <userDefinedPort start="false" port="874"/><!-- port=874 -->
 33         <timeout start="false" time="100"/><!-- timeout=100 -->
 34         <ssh start="false"/>
 35     </rsync>
 #后台独立运行web2对应服务
 [root@NFS sersync]$ sersync2 -dro /usr/local/sersync/jpress.xml
 
 
 #为了防止服务器重启后手动执行的服务断开,将执行命令写进文件中,随开机启动
 [root@NFS ~]$ echo -e "/usr/local/sersync/sersync2 -dro /usr/local/sersync/confxml.xml &> /dev/null\n/usr/local/sersync/sersync2 -dro /usr/local/sersync/jpress.xml &> /dev/null" > /etc/profile.d/sersync2.sh
 [root@NFS ~]$ chmod +x /etc/profile.d/sersync2.sh
  1. 部署nfs备份服务器
#在10.0.0.78 NFS备份服务器以独立服务方式运行rsync并实现验证功能
[root@NFS-bak ~]$ yum -y install rsync-daemon

#创建备份目录
[root@NFS-bak ~]$ mkdir /data/backup -p
[root@NFS-bak ~]$ mkdir /data/web2-backup

#修改配置文件,添加以下信息
[root@NFS-bak ~]$ vi /etc/rsyncd.conf
uid = www    #指定以哪个用户来访问共享目录,将之指定为生成的文件所有者,默认是nobody
gid = www
max connections = 0
ignore errors
exclude = lost+found/
log file = /var/log/rsyncd.log
pid file = /var/run/rsyncd.pid
lock file = /var/run/rsyncd.lock
reverse lookup = no

[backup]  #每个模块名对应一个不同的path目录,如果同名后面模块生效
path = /data/backup/
comment = backup dir
read only = no   #默认是yes,即只读
auth users = rsyncuser   #默认anonymous可以访问rsync服务器,主服务器中指定的用户
secrets file = /etc/rsync.pas

[web2-backup]
path = /data/web2-backup/
comment = backup dir
read only = no
auth users = rsyncuser
secrets file = /etc/rsync.pas


#创建验证文件
[root@NFS-bak ~]$ echo "rsyncuser:lgq123456" > /etc/rsync.pas

#创建传输用户
[root@NFS-bak ~]$ chmod 600 /etc/rsync.pas
[root@NFS-bak ~]$ groupadd -g 666 www
[root@NFS-bak ~]$ useradd -u 666 www -g 666
[root@NFS-bak ~]$ chown www.www /data/backup/ -R
[root@NFS-bak ~]$ chown -R www.www /data/web2-backup/

#重载配置
[root@NFS-bak ~]$ rsync --daemon 

#放进文件中,随主机开启自启动
[root@NFS-bak ~]$ echo "rsync --daemon" > /etc/profile.d/rsync.sh
[root@NFS-bak ~]$ chmod +x /etc/profile.d/rsync.sh
  1. 测试是否主备同步数据
#在NFS主服务器上共享目录创建一个test.txt文件,查看备份服务器上是否同步
[root@NFS ~]$ cd /data/www/
[root@NFS www]$ touch test.txt
[root@NFS www]$ ll
total 0
-rw-r--r-- 1 root root 0 Mar  9 22:23 test.txt

[root@NFS-bak ~]$ ll /data/backup/
total 0
-rw-r--r-- 1 www www 0 Mar  9 22:23 test.txt

4 在10.0.0.48和10.0.0.58主机上搭建MySQL主从节点

  • 主节点:10.0.0.48
  • 从节点:10.0.0.58
  1. 搭建主节点
#安装mysql
[root@mysql-master ~]$ yum -y install mysql-server

#创建二进制日志存放路径,并在配置文件中指定路径以及日子文件的前缀
[root@mysql-master ~]$ mkdir /data/binlog
[root@mysql-master ~]$ chown mysql. /data/binlog/

#设置配置文件,并启动服务
[root@mysql-master ~]$ cat /etc/my.cnf 
[mysqld]
server-id=48 
log_bin=/data/binlog/mysql-bin

[root@mysql-master ~]$ systemctl enable --now mysqld

#创建复制用户以及授权
[root@mysql-master ~]$ mysql -uroot -plgq123456 -e "create user 'repluser'@'10.0.0.%' identified by 'lgq123456';"
[root@mysql-master ~]$ mysql -uroot -plgq123456 -e "grant replication slave on *.* to 'repluser'@'10.0.0.%';"

#创建kodbox对应数据库以及账号
[root@mysql-master ~]$ mysql -uroot -plgq123456 -e "create database kodbox;"
[root@mysql-master ~]$ mysql -uroot -plgq123456 -e "create user kodbox@'10.0.0.%' identified by 'lgq123456';"
[root@mysql-master ~]$ mysql -uroot -plgq123456 -e "grant all on kodbox.* to  kodbox@'10.0.0.%';"

#创建web2业务对应的数据库和用户
[root@mysql-master ~]$ mysql -uroot -plgq123456 -e "create database jpress;"
[root@mysql-master ~]$ mysql -uroot -plgq123456 -e "create user jpress@'10.0.0.%' identified by '123456';"
[root@mysql-master ~]$ mysql -uroot -plgq123456 -e "grant all on jpress.* to jpress@'10.0.0.%';"


#进行完全备份
[root@mysql-master ~]$ mysqldump -uroot -plgq123456 -A -F --single-transaction --master-data=1 > full_backup.sql

#拷贝备份数据到从节点
[root@mysql-master ~]$ scp full_backup.sql 10.0.0.58:
  1. 搭建从节点
#安装
[root@mysql-slave ~]$ yum -y install mysql-server
#修改配置文件,并启动
[root@mysql-slave ~]$ vi /etc/my.cnf
#添加下面信息
[mysqld]
server-id=58
read-only

[root@mysql-slave ~]$ systemctl enable --now mysqld

#修改备份文件,在change master to中添加主节点信息
[root@mysql-slave ~]$ vi full_backup.sql
......
CHANGE MASTER TO 
    MASTER_HOST='10.0.0.48',  		#添上主节点ip地址
    MASTER_USER='repluser',			#添上在主节点创建的账号
    MASTER_PASSWORD='lgq123456',	#添上账号密码
    MASTER_PORT=3306,				#添上端口号
    MASTER_LOG_FILE='mysql-bin.000003', 
    MASTER_LOG_POS=157;
......

#还原备份
###暂时关闭二进制日志
[root@mysql-slave ~]$ mysql
mysql> set sql_log_bin=0;
###还原
mysql> source /root/full_backup.sql;
##开启主从节点的链接线程
mysql> start slave;
##查看状态
mysql> show slave status\G
*************************** 1. row ***************************
               Slave_IO_State: Waiting for source to send event
                  Master_Host: 10.0.0.48
                  Master_User: repluser
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: mysql-bin.000003
          Read_Master_Log_Pos: 157
               Relay_Log_File: mysql-slave-relay-bin.000002
                Relay_Log_Pos: 326
        Relay_Master_Log_File: mysql-bin.000003
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
              Replicate_Do_DB: 
          Replicate_Ignore_DB: 
           Replicate_Do_Table: 
       Replicate_Ignore_Table: 
      Replicate_Wild_Do_Table: 
  Replicate_Wild_Ignore_Table: 
                   Last_Errno: 0
                   Last_Error: 
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 157
              Relay_Log_Space: 542
              Until_Condition: None
               Until_Log_File: 
                Until_Log_Pos: 0
           Master_SSL_Allowed: No
           Master_SSL_CA_File: 
           Master_SSL_CA_Path: 
              Master_SSL_Cert: 
            Master_SSL_Cipher: 
               Master_SSL_Key: 
        Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
                Last_IO_Errno: 0
                Last_IO_Error: 
               Last_SQL_Errno: 0
               Last_SQL_Error: 
  Replicate_Ignore_Server_Ids: 
             Master_Server_Id: 48
                  Master_UUID: bdcb41ce-be61-11ed-808a-000c2924e25d
             Master_Info_File: mysql.slave_master_info
                    SQL_Delay: 0
          SQL_Remaining_Delay: NULL
      Slave_SQL_Running_State: Replica has read all relay log; waiting for more updates
           Master_Retry_Count: 86400
                  Master_Bind: 
      Last_IO_Error_Timestamp: 
     Last_SQL_Error_Timestamp: 
               Master_SSL_Crl: 
           Master_SSL_Crlpath: 
           Retrieved_Gtid_Set: 
            Executed_Gtid_Set: 
                Auto_Position: 0
         Replicate_Rewrite_DB: 
                 Channel_Name: 
           Master_TLS_Version: 
       Master_public_key_path: 
        Get_master_public_key: 0
            Network_Namespace: 
1 row in set, 1 warning (0.01 sec)
  1. 测试主从是否同步
#在主节点上创建一个测试数据库
mysql> create database t1;
Query OK, 1 row affected (0.00 sec)

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| kodbox             |
| mysql              |
| performance_schema |
| sys                |
| t1                 |
+--------------------+
6 rows in set (0.00 sec)

#在从节点查看是否存在
mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| kodbox             |
| mysql              |
| performance_schema |
| sys                |
| t1                 |
+--------------------+
6 rows in set (0.01 sec)
##说明主从复制已经可以同步了

5 在10.0.0.88主机上部署redis

#安装redis
[root@redis ~]$ yum -y install redis
#修改配置文件
[root@redis ~]$ vi /etc/redis.conf 
bind 0.0.0.0 #将此行的127.0.0.1改为0.0.0.0,实现远程访问
[root@redis ~]$ systemctl enable --now redis

6 搭建 www.yanlinux.org web1业务(可道云业务)

6.1 在10.0.0.28上搭建nginx和php-fpm

# 1.利用脚本一键编译安装nginx
[root@web1 ~]$ cat install_nginx.sh 
#!/bin/bash

OS_TYPE=`awk -F'[ "]' '/^NAME/{print $2}' /etc/os-release`
OS_VERSION=`awk -F'[".]' '/^VERSION_ID/{print $2}' /etc/os-release`
CPU=`lscpu |awk '/^CPU\(s\)/{print $2}'`
SRC_DIR=/usr/local/src
read -p "$(echo -e '\033[1;32m请输入下载的版本号:\033[0m')" NUM
NGINX_FILE=nginx-${NUM}
NGINX_INSTALL_DIR=/apps/nginx

color () {
    RES_COL=60
    MOVE_TO_COL="echo -en \\033[${RES_COL}G"
    SETCOLOR_SUCCESS="echo -en \\033[1;32m"
    SETCOLOR_FAILURE="echo -en \\033[1;31m"
    SETCOLOR_WARNING="echo -en \\033[1;33m"
    SETCOLOR_NORMAL="echo -en \E[0m"
    echo -n "$1" && $MOVE_TO_COL
    echo -n "["
    if [ $2 = "success" -o $2 = "0" ] ;then
        ${SETCOLOR_SUCCESS}
        echo -n $"  OK  "
    elif [ $2 = "failure" -o $2 = "1"  ] ;then
        ${SETCOLOR_FAILURE}
        echo -n $"FAILED"
    else
        ${SETCOLOR_WARNING}
        echo -n $"WARNING"
    fi
    ${SETCOLOR_NORMAL}
    echo -n "]"
    echo
}

#下载源码
wget_package(){
    [ -e ${NGINX_INSTALL_DIR} ] && { color "nginx 已安装,请卸载后再安装" 1; exit; }
    cd ${SRC_DIR}
    if [ -e ${NGINX_FILE}.tar.gz ];then
        color "源码包已经准备好" 0
    else
        color "开始下载源码包" 0
        wget http://nginx.org/download/${NGINX_FILE}.tar.gz
        [ $? -ne 0 ] && { color "下载 ${NGINX_FILE}.tar.gz文件失败" 1; exit; }
    fi
}

#编译安装
install_nginx(){
    color "开始安装nginx" 0
    if id nginx &> /dev/null;then
        color "nginx用户已经存在" 1
    else
        useradd -s /sbin/nologin -r nginx
        color "nginx用户账号创建完成" 0
    fi

    color "开始安装nginx依赖包" 0
    if [ $OS_TYPE == "Centos" -a ${OS_VERSION} == '7' ];then
        yum -y install make gcc pcre-devel openssl-devel zlib-devel perl-ExtUtils-Embed
    elif [ $OS_TYPE == "Centos" -a ${OS_VERSION} == '8' ];then
        yum -y install make gcc-c++ libtool pcre pcre-devel zlib zlib-devel openssl openssl-devel perl-ExtUtils-Embed
    elif [ $OS_TYPE == "Rocky" ];then
        yum -y install make gcc libtool pcre pcre-devel zlib zlib-devel openssl openssl-devel perl-ExtUtils-Embed
    elif [ $OS_TYPE == "Ubuntu" ];then
        apt update
        apt -y install make gcc libpcre3 libpcre3-dev openssl libssl-dev zlib1g-dev 
    else
        color '不支持此系统!'  1
        exit
    fi

    #开始编译安装
    color "开始编译安装nginx" 0
    cd $SRC_DIR
    tar xf ${NGINX_FILE}.tar.gz
    cd ${SRC_DIR}/${NGINX_FILE}
    ./configure --prefix=${NGINX_INSTALL_DIR} --user=nginx --group=nginx --with-http_ssl_module --with-http_v2_module --with-http_realip_module --with-http_stub_status_module --with-http_gzip_static_module --with-pcre --with-stream --with-stream_ssl_module --with-stream_realip_module
    make -j ${CPU} && make install
    [ $? -eq 0 ] && color "nginx 编译安装成功" 0 ||  { color "nginx 编译安装失败,退出!" 1 ;exit; }
    ln -s ${NGINX_INSTALL_DIR}/sbin/nginx /usr/sbin/ &> /dev/null

    #创建service文件
    cat > /lib/systemd/system/nginx.service <<EOF
[Unit]
Description=The nginx HTTP and reverse proxy server
After=network.target remote-fs.target nss-lookup.target

[Service]
Type=forking
PIDFile=${NGINX_INSTALL_DIR}/logs/nginx.pid
ExecStartPre=/bin/rm -f ${NGINX_INSTALL_DIR}/logs/nginx.pid
ExecStartPre=${NGINX_INSTALL_DIR}/sbin/nginx -t
ExecStart=${NGINX_INSTALL_DIR}/sbin/nginx
ExecReload=/bin/kill -s HUP \$MAINPID
KillSignal=SIGQUIT
TimeoutStopSec=5
KillMode=process
PrivateTmp=true                                                                                        
LimitNOFILE=100000

[Install]
WantedBy=multi-user.target
EOF

    #启动服务
    systemctl enable --now nginx &> /dev/null
    systemctl is-active nginx &> /dev/null ||  { color "nginx 启动失败,退出!" 1 ; exit; }
    color "nginx 安装完成" 0

}

wget_package
install_nginx

##执行脚本安装nginx
[root@web1 ~]$ sh install_nginx.sh
[root@web1 ~]$ ss -ntl
State      Recv-Q      Send-Q           Local Address:Port           Peer Address:Port     Process     
LISTEN     0           128                    0.0.0.0:80                  0.0.0.0:*


# 2.安装配置php-fpm
[root@web1 ~]$ yum -y install php-fpm 
##安装php-mysql 以及php-redis所依赖的包
[root@web1 ~]$ php-mysqlnd php-json php-cli php-devel

##下载php-redis
[root@web1 ~]$ wget https://pecl.php.net/get/redis-5.3.7.tgz -P /usr/local/src/
[root@web1 ~]$ cd /usr/local/src/
[root@web1 src]$ tar xf redis-5.3.7.tgz 
[root@web1 src]$ cd redis-5.3.7/
[root@web1 redis-5.3.7]$ phpize 
Configuring for:
PHP Api Version:         20170718
Zend Module Api No:      20170718
Zend Extension Api No:   320170718
[root@web1 redis-5.3.7]$ ./configure
[root@web1 redis-5.3.7]$ make && make install

##创建php支持redis扩展的配置文件
[root@web1 redis-5.3.7]$ vi /etc/php.d/31-redis.ini
extension=redis #加入此行

[root@web1 redis-5.3.7]$ cd

#修改php上传限制配置
[root@web1 ~]$ vi /etc/php.ini
post_max_size = 200M 		#修改为200M
upload_max_filesize = 200M 	#改为200M,实现大文件上传

#修改配置文件
[root@web1 ~]$ vi /etc/php-fpm.d/www.conf
user = nginx 					#修改为nginx
group = nginx 					#修改为nginx
;listen = /run/php-fpm/www.sock #注释此行
listen = 127.0.0.1:9000 		#添加此行,监控本机的9000端口
pm.status_path = /fpm_status 	#取消此行的注释,并改为fpm_status,防止与nginx服务的status冲突
ping.path = /ping 				#取消此行的注释
ping.response = pong 			#取消此行的注释

##启动服务
[root@web1 ~]$ systemctl enable --now php-fpm


# 3.配置nginx虚拟主机配置文件
##为了方便管理不同的业务,nginx支持子配置文件
##创建子配置文件目录
[root@web1 ~]$ mkdir /apps/nginx/conf/conf.d
[root@web1 ~]$ vi /apps/nginx/conf/nginx.conf
	include /apps/nginx/conf/conf.d/*.conf; #在http语句块最后一行添加上这一行
##创建业务配置文件
[root@web1 ~]$ cat /apps/nginx/conf/conf.d/www.yanlinux.org.conf
server {
    listen 80;
    server_name www.yanlinux.org;
    client_max_body_size 100M;
    server_tokens off;
    location / {
        root /data/kodbox/; 
        index index.php index.html index.htm;
    }
    location ~ \.php$ {
        root           /data/kodbox/;
        fastcgi_pass   127.0.0.1:9000;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include        fastcgi_params;
        fastcgi_hide_header X-Powered-By;
    }
    location ~ ^/(ping|fpm_status)$ {
        fastcgi_pass   127.0.0.1:9000;
        fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include        fastcgi_params;
    }
}


# 4.重启服务
[root@web1 ~]$ systemctl restart nginx.service php-fpm.service

查看状态php状态页,测试服务搭建成功

6.2 部署kodbox

##安装可道云(kodbox)所需的依赖包
[root@web1 ~]$ yum -y install php-mbstring php-xml php-gd

#下载源码包
[root@web1 ~]$ wget https://static.kodcloud.com/update/download/kodbox.1.35.zip

[root@web1 ~]$ unzip kodbox.1.35.zip -d /data/kodbox

[root@web1 ~]$ chown -R nginx.nginx /data/kodbox/

6.3 挂载NFS服务器,实现网站数据远程备份

#安装nfs-utils搭建挂载NFS
[root@web1 ~]$ yum -y install nfs-utils
[root@web1 ~]$ showmount -e 10.0.0.68
Export list for 10.0.0.68:
/data/web2 *
/data/www *

#实现永久挂载,添加以下挂载信息,利用可道云上传的数据都会存放在/data/kodbox/data/files目录下,所以讲这个目录挂载nfs
[root@web1 ~]$ vi /etc/fstab
10.0.0.68:/data/www     /data/kodbox/data/files nfs     _netdev         0 0
[root@web1 ~]$ mount -a
[root@web1 ~]$ df -h|grep data
10.0.0.68:/data/www   70G  2.3G   68G   4% /data/kodbox/data/files

往可道云上上传文件MyHotkeyScript.ahk,测试NFS主备服务是否都可以得到数据

#查看web1服务器上是否上传了数据
[root@web1 ~]$ ll /data/kodbox/data/files/202303/09_079920df/
total 4
-rwxrwxrwx 1 666 666 1491 Mar  9 22:51 MyHotkeyScript.ahk

#在NFS主服务器上查看
[root@NFS ~]$ ll /data/www/202303/09_079920df/
total 4
-rwxrwxrwx 1 www www 1491 Mar  9 22:51 MyHotkeyScript.ahk

#在NFS备份服务器上查看
[root@NFS-bak ~]$ ll /data/backup/202303/09_079920df/
total 4
-rwxrwxrwx 1 www www 1491 Mar  9 22:51 MyHotkeyScript.ahk

7 部署www.yanlinux.edu web2业务(JPress)

7.1 在10.0.0.38主机上搭建tomcat

#利用脚本一键安装jdk以及tomcat
[root@web2 ~]$ cat install_tomcat_jdk.sh
#!/bin/bash
DIR=`pwd`
read -p "$(echo -e '\033[1;32m请输入JDK版本号:\033[0m')" JDK_VERSION
read -p "$(echo -e '\033[1;32m请输入Tomcat版本号:\033[0m')" TOMCAT_VERSION
JDK_FILE="jdk-${JDK_VERSION}-linux-x64.tar.gz"
TOMCAT_FILE="apache-tomcat-${TOMCAT_VERSION}.tar.gz"
INSTALL_DIR="/usr/local"

color () {
    RES_COL=60
    MOVE_TO_COL="echo -en \\033[${RES_COL}G"
    SETCOLOR_SUCCESS="echo -en \\033[1;32m"
    SETCOLOR_FAILURE="echo -en \\033[1;31m"
    SETCOLOR_WARNING="echo -en \\033[1;33m"
    SETCOLOR_NORMAL="echo -en \E[0m"
    echo -n "$2" && $MOVE_TO_COL
    echo -n "["
    if [ $1 = "success" -o $1 = "0" ] ;then
        ${SETCOLOR_SUCCESS}
        echo -n $"  OK  "
    elif [ $1 = "failure" -o $1 = "1"  ] ;then
        ${SETCOLOR_FAILURE}
        echo -n $"FAILED"
    else
        ${SETCOLOR_WARNING}
        echo -n $"WARNING"
    fi
    ${SETCOLOR_NORMAL}
    echo -n "]"
    echo
}

install_jdk(){
    if ! [ -f "${DIR}/${JDK_FILE}" ];then
        color 1 "${JDK_FILE}不存在,请去官网下载"
        exit;
    elif [ -f ${INSTALL_DIR}/jdk ];then
        color 1 "JDK已经安装"
        exit;
    else
        [ -d "${INSTALL_DIR}" ] || mkdir -pv ${INSTALL_DIR}
    fi
    tar xf ${DIR}/${JDK_FILE} -C ${INSTALL_DIR}
    cd ${INSTALL_DIR} && ln -s jdk* jdk

    cat > /etc/profile.d/jdk.sh <<EOF
export JAVA_HOME=${INSTALL_DIR}/jdk
#export JRE_HOME=\$JAVA_HOME/jre
#export CLASSPATH=.:\$JAVA_HOME/lib/:\$JRE_HOME/lib/
export PATH=\$PATH:\$JAVA_HOME/bin
EOF
    . /etc/profile.d/jdk.sh
    java -version && color 0 "JDK安装完成" || { color 1 "JDK安装失败"; exit; }
}

install_tomcat(){
    if ! [ -f "${DIR}/${TOMCAT_FILE}" ];then
        color 1 "${TOMCAT_FILE}不存在,请去官网下载"
        exit;
    elif [ -f ${INSTALL_DIR}/tomcat ];then
        color 1 "tomcat已经安装"
        exit;
    else
        [ -d "${INSTALL_DIR}" ] || mkdir -pv ${INSTALL_DIR}
    fi
    
    tar xf ${DIR}/${TOMCAT_FILE} -C ${INSTALL_DIR}
    cd ${INSTALL_DIR} && ln -s apache-tomcat-*/ tomcat
    echo "PATH=${INSTALL_DIR}/tomcat/bin:"'$PATH' > /etc/profile.d/tomcat.sh
    id tomcat &> /dev/null || useradd -r -s /sbin/nologin tomcat

    cat > ${INSTALL_DIR}/tomcat/conf/tomcat.conf <<EOF
JAVA_HOME=${INSTALL_DIR}/jdk
EOF
    
    chown -R tomcat.tomcat ${INSTALL_DIR}/tomcat/

    cat > /lib/systemd/system/tomcat.service <<EOF
[Unit]
Description=Tomcat
#After=syslog.target network.target remote-fs.target nss-lookup.target
After=syslog.target network.target 

[Service]
Type=forking
EnvironmentFile=${INSTALL_DIR}/tomcat/conf/tomcat.conf
ExecStart=${INSTALL_DIR}/tomcat/bin/startup.sh
ExecStop=${INSTALL_DIR}/tomcat/bin/shutdown.sh
RestartSec=3
PrivateTmp=true
User=tomcat
Group=tomcat

[Install]
WantedBy=multi-user.target
EOF

    systemctl daemon-reload
    systemctl enable --now tomcat.service &> /dev/null
    systemctl is-active tomcat.service &> /dev/null && color 0 "TOMCAT 安装完成" || { color 1 "TOMCAT 安装失败" ; exit; }
}


install_jdk
install_tomcat

[root@web2 ~]$ sh install_tomcat.sh 
请输入JDK版本号:8u321
请输入Tomcat版本号:9.0.59
java version "1.8.0_321"
Java(TM) SE Runtime Environment (build 1.8.0_321-b07)
Java HotSpot(TM) 64-Bit Server VM (build 25.321-b07, mixed mode)
JDK安装完成                                                [  OK  ]
TOMCAT 安装完成                                            [  OK  ]

#创建虚拟主机
[root@web2 ~]$ vi /usr/local/tomcat/conf/server.xml
            pattern="%h %l %u %t &quot;%r&quot; %s %b" />
      </Host>
      #在这一行之后添加下面几行信息
      <Host name="www.yanlinux.edu" appBase="/data/jpress/" unpackWARs="true" autoDeploy="true">
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="jpress_access_log" suffix=".txt"                                                
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />
      </Host>
      #以上信息就是虚拟主机的配置信息
    </Engine>
  </Service>
</Server>

#准备虚拟主机的数据目录,tomcat默认会在ROOT目录中找,所以需要将应用数据布置到这里面就可以避免在URL中添加应用目录来访问了。
[root@web2 ~]$ mkdir /data/webapps/ROOT -p
[root@web2 ~]$ chown -R tomcat.tomcat /data/webapps

[root@web2 ~]$ systemctl restart tomcat.service

7.2 部署nginx

#利用6.1中的安装nginx脚本来安装
[root@web2 ~]$ sh install_nginx.sh

#创建子配置目录
[root@web2 ~]$ mkdir /apps/nginx/conf/conf.d
[root@web2 ~]$ vi /apps/nginx/conf/nginx.conf
#在主配置文件中引入子配置目录
[root@web2 ~]$ tail -n2 /apps/nginx/conf/nginx.conf
    include /apps/nginx/conf/conf.d/*.conf;
}

#创建业务2配置文件
[root@web2 ~]$ cat /apps/nginx/conf/conf.d/www.yanlinux.edu.conf
server {
    listen 80;
    server_name www.yanlinux.edu;
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

[root@web2 ~]$ nginx -t
nginx: the configuration file /apps/nginx/conf/nginx.conf syntax is ok
nginx: configuration file /apps/nginx/conf/nginx.conf test is successful
[root@web2 ~]$ nginx -s reload

7.3 部署JPress应用

#上官网http://www.jpress.io/下载war包,上传到服务器
[root@web2 ~]$ cp jpress-v4.0.7.war /usr/local/tomcat/webapps/jpress.war
[root@web2 ~]$ cd /usr/local/tomcat/webapps/
#war包传到tomcat目录下就会自动解包,
[root@web2 webapps]$ ls
docs  examples  host-manager  jpress  jpress.war  manager  ROOT

#然后将jpress/目录下的内容拷贝到6.7.1创建tomcat虚拟主机www.yanlinux.edu的数据目录中
[root@web2 ~]$ cp -a /usr/local/tomcat/webapps/jpress/* /data/webapps/ROOT/


#数据库账号已经在6.4中创建直接连接就可以

浏览器访问

7.4 挂载NFS服务器

[root@web2 ~]$ yum -y install nfs-utils
[root@web2 ~]$ showmount -e 10.0.0.68
Export list for 10.0.0.68:
/data/web2 *
/data/www  *

#永久挂载,添加挂载信息
[root@web2 ~]$ vi /etc/fstab
10.0.0.68:/data/web2    /data/webapps/ROOT/attachment/  nfs _netdev     0 0
[root@web2 ~]$ mount -a
[root@web2 ~]$ df -h |grep data
10.0.0.68:/data/web2   70G  2.2G   68G   4% /data/webapps/ROOT/attachment

#发布文章,添加一张照片测试
[root@web2 ~]$ ll /data/webapps/ROOT/attachment/20230310/
total 560
-rw-r--r-- 1 666 666 569655 Mar 10 11:10 2974a6d37fb04ebfab8c7816d0a8dadd.png

#NFS服务器上查看
[root@NFS ~]$ ll /data/web2/20230310/
total 560
-rw-r--r-- 1 www www 569655 Mar 10 11:10 2974a6d37fb04ebfab8c7816d0a8dadd.png

#NFS备份服务器查看
[root@NFS-bak ~]$ ll /data/web2-backup/20230310/
total 560
-rw-r--r-- 1 www www 569655 Mar 10 11:10 2974a6d37fb04ebfab8c7816d0a8dadd.png

7.5 利用redis实现session共享

#准备从官网下载两个jar包,上传到lib目录下,
##下载地址https://github.com/redisson/redisson/tree/master/redisson-tomcat
[root@web2 ~]$ cd /usr/local/tomcat/lib/
[root@web2 lib]$ ls redisson-*
redisson-all-3.20.0.jar  redisson-tomcat-9-3.20.0.jar

#在context.xml最后一行之前加上以下内容
[root@web2 lib]$ vi ../conf/context.xml
    #添加以下信息
    <Manager className="org.redisson.tomcat.RedissonSessionManager"
             configPath="${catalina.base}/conf/redisson.conf"
             readMode="REDIS" updateMode="DEFAULT" broadcastSessionEvents="false"
             keyPrefix=""/>
    #以上信息就是添加的内容
</Context>


#创建redisson.conf文件
[root@web2 lib]$ vi ../conf/redisson.conf
{
    "singleServerConfig":{
        "idleConnectionTimeout":10000,
        "connectTimeout":10000,
        "timeout":3000,
        "retryAttempts":3,
        "retryInterval":1500,
        "password":null,
        "subscriptionsPerConnection":5,
        "clientName":null,
        "address": "redis://10.0.0.88:6379", #redis服务器地址
        "subscriptionConnectionMinimumIdleSize":1,
        "subscriptionConnectionPoolSize":50,
        "connectionMinimumIdleSize":32,
        "connectionPoolSize":64,
        "database":0,
        "dnsMonitoringInterval":5000
    },
    "threads":0,
    "nettyThreads":0,
    "codec":{
        "class":"org.redisson.codec.JsonJacksonCodec"
    },
    "transportMode":"NIO"
}

[root@web2 lib]$ systemctl restart tomcat.service 

8 KeepAlived+HAProxy服务搭建,实现整体架构

分别在
10.0.0.8

10.0.0.18
两台rocky主机上编译安装
keepalived

HAProxy
两个服务,实现高可用。

  1. 编译安装keepalived
#ka1节点编译安装
# 1.安装依赖
##centos和rocky
[root@ka1 ~]$ yum -y install gcc curl openssl-devel libnl3-devel net-snmp-devel
##ubuntu所需要的依赖下面两种:
##ubuntu18.04
[root@ubuntu1804 ~]$ apt -y install gcc curl openssl libssl-dev libpopt-dev daemon build-essential
##ubuntu20.04
[root@ubuntu2004 ~]$ apt -y install make gcc ipvsadm build-essential pkg-config automake autoconf libipset-dev libnl-3-dev libnl-genl-3-dev libssl-dev libxtables-dev libip4tc-dev libip6tc-dev libipset-dev libmagic-dev libsnmp-dev libglib2.0-dev libpcre2-dev libnftnl-dev libmnl-dev libsystemd-dev



# 2.下载源码包
[root@ka1 ~]$ wget https://keepalived.org/software/keepalived-2.2.7.tar.gz

##解压
[root@ka1 ~]$ tar xf keepalived-2.2.7.tar.gz



# 3.编译安装
[root@ka1 ~]$ cd keepalived-2.2.7/

#选项--disable-fwmark 可用于禁用iptables规则,可访止VIP无法访问,无此选项默认会启用iptables规则
[root@ka1 keepalived-2.2.7]$ ./configure --prefix=/usr/local/keepalived --disable-fwmark
[root@ka1 keepalived-2.2.7]$ make -j 2 && make install

##验证版本信息
[root@ka1 keepalived-2.2.7]$ /usr/local/keepalived/sbin/keepalived -v
Keepalived v2.2.7 (01/16,2022)

Copyright(C) 2001-2022 Alexandre Cassen, <acassen@gmail.com>

Built with kernel headers for Linux 4.18.0
Running on Linux 4.18.0-348.el8.0.2.x86_64 #1 SMP Sun Nov 14 00:51:12 UTC 2021
Distro: Rocky Linux 8.5 (Green Obsidian)

configure options: --prefix=/usr/local/keepalived --disable-fwmark

Config options:  LVS VRRP VRRP_AUTH VRRP_VMAC OLD_CHKSUM_COMPAT INIT=systemd

System options:  VSYSLOG MEMFD_CREATE IPV4_DEVCONF LIBNL3 RTA_ENCAP RTA_EXPIRES RTA_NEWDST RTA_PREF FRA_SUPPRESS_PREFIXLEN FRA_SUPPRESS_IFGROUP FRA_TUN_ID RTAX_CC_ALGO RTAX_QUICKACK RTEXT_FILTER_SKIP_STATS FRA_L3MDEV FRA_UID_RANGE RTAX_FASTOPEN_NO_COOKIE RTA_VIA FRA_PROTOCOL FRA_IP_PROTO FRA_SPORT_RANGE FRA_DPORT_RANGE RTA_TTL_PROPAGATE IFA_FLAGS LWTUNNEL_ENCAP_MPLS LWTUNNEL_ENCAP_ILA NET_LINUX_IF_H_COLLISION LIBIPTC_LINUX_NET_IF_H_COLLISION LIBIPVS_NETLINK IPVS_DEST_ATTR_ADDR_FAMILY IPVS_SYNCD_ATTRIBUTES IPVS_64BIT_STATS VRRP_IPVLAN IFLA_LINK_NETNSID GLOB_BRACE GLOB_ALTDIRFUNC INET6_ADDR_GEN_MODE VRF



# 4.创建service文件
##默认源码包中会有unit文件,只需要将提供的service文件拷贝到/lib/systemd/system/目录下即可
[root@ka1 keepalived-2.2.7]$ cp ./keepalived/keepalived.service /lib/systemd/system/
[root@ka1 keepalived-2.2.7]$ cat /lib/systemd/system/keepalived.service 
[Unit]
Description=LVS and VRRP High Availability Monitor
After=network-online.target syslog.target 
Wants=network-online.target 
Documentation=man:keepalived(8)
Documentation=man:keepalived.conf(5)
Documentation=man:genhash(1)
Documentation=https://keepalived.org

[Service]
Type=forking
PIDFile=/run/keepalived.pid
KillMode=process
EnvironmentFile=-/usr/local/keepalived/etc/sysconfig/keepalived
ExecStart=/usr/local/keepalived/sbin/keepalived  $KEEPALIVED_OPTIONS
ExecReload=/bin/kill -HUP $MAINPID

[Install]
WantedBy=multi-user.target


# 5.创建配置文件
##编译目录下会自动生成示例配置文件,需要在/etc目录下建一个keepalived目录存放配置文件。然后将其中配置VRRP以及real-server的示例信息删除,只留下global_defs配置语块即可。
[root@ka1 keepalived-2.2.7]$ mkdir /etc/keepalived
[root@ka1 keepalived-2.2.7]$ cp /usr/local/keepalived/etc/keepalived/keepalived.conf.sample /etc/keepalived/keepalived.conf
##按自己需求修改示例配置文件
[root@ka1 keepalived-2.2.7]$ vi /etc/keepalived/keepalived.conf
global_defs {
   notification_email {
     acassen@firewall.loc
     failover@firewall.loc
     sysadmin@firewall.loc
   }
   notification_email_from Alexandre.Cassen@firewall.loc
   smtp_server 192.168.200.1
   smtp_connect_timeout 30
   router_id ka1    #每个keepalived主机唯一标识,建议使用当前主机名,如果多节点重名可能会影响切换脚本执行。在另一台keepalived主机ka2上,应该改为ka2
   vrrp_skip_check_adv_addr
   vrrp_strict
   vrrp_garp_interval 0
   vrrp_gna_interval 0
   vrrp_mcast_group4 230.1.1.1
}
include /etc/keepalived/conf.d/*.conf 	#当生产环境复杂时, /etc/keepalived/keepalived.conf 文件中内容过多,不易管理,可以将不同集群的配置,比如:不同集群的VIP配置放在独立的子配置文件中。利用include指令可以实现包含子配置文件

##创建子配置文件目录
[root@ka1 keepalived-2.2.7]$ mkdir /etc/keepalived/conf.d/



# 6.启动服务
[root@ka1 keepalived-2.2.7]$ systemctl daemon-reload 
[root@ka1 keepalived-2.2.7]$ systemctl enable --now keepalived.service
[root@ka1 keepalived-2.2.7]$ systemctl is-active keepalived
active



# 7.在ka2节点上按照ka1节点操作进行编译安装
[root@ka2 ~]$ systemctl is-active keepalived.service 
active

[root@ka2 ~]$ /usr/local/keepalived/sbin/keepalived -v
Keepalived v2.2.7 (01/16,2022)

Copyright(C) 2001-2022 Alexandre Cassen, <acassen@gmail.com>

Built with kernel headers for Linux 4.18.0
Running on Linux 4.18.0-348.el8.0.2.x86_64 #1 SMP Sun Nov 14 00:51:12 UTC 2021
Distro: Rocky Linux 8.5 (Green Obsidian)

configure options: --prefix=/usr/local/keepalived --disable-fwmark

Config options:  LVS VRRP VRRP_AUTH VRRP_VMAC OLD_CHKSUM_COMPAT INIT=systemd

System options:  VSYSLOG MEMFD_CREATE IPV4_DEVCONF LIBNL3 RTA_ENCAP RTA_EXPIRES RTA_NEWDST RTA_PREF FRA_SUPPRESS_PREFIXLEN FRA_SUPPRESS_IFGROUP FRA_TUN_ID RTAX_CC_ALGO RTAX_QUICKACK RTEXT_FILTER_SKIP_STATS FRA_L3MDEV FRA_UID_RANGE RTAX_FASTOPEN_NO_COOKIE RTA_VIA FRA_PROTOCOL FRA_IP_PROTO FRA_SPORT_RANGE FRA_DPORT_RANGE RTA_TTL_PROPAGATE IFA_FLAGS LWTUNNEL_ENCAP_MPLS LWTUNNEL_ENCAP_ILA NET_LINUX_IF_H_COLLISION LIBIPTC_LINUX_NET_IF_H_COLLISION LIBIPVS_NETLINK IPVS_DEST_ATTR_ADDR_FAMILY IPVS_SYNCD_ATTRIBUTES IPVS_64BIT_STATS VRRP_IPVLAN IFLA_LINK_NETNSID GLOB_BRACE GLOB_ALTDIRFUNC INET6_ADDR_GEN_MODE VRF
  1. 编译安装HAProxy服务

    编译安装HAProxy 2.6 LTS版本,更多源码包下载地址:
    http://www.haproxy.org/download/

    依赖lua环境,由于CentOS7 之前版本自带的lua版本比较低并不符合HAProxy要求的lua最低版本(5.3)的要求,因此需要编译安装较新版本的lua环境,然后才能编译安装HAProxy。

#ka1节点安装HAProxy
# 1.安装依赖环境
##centos或rocky
[root@ka1 ~]$ yum -y install gcc make gcc-c++ glibc glibc-devel pcre pcre-devel openssl openssl-devel systemd-devel libtermcap-devel ncurses-devel libevent-devel readline-devel
##ubuntu
apt -y install gcc make openssl libssl-dev libpcre3 libpcre3-dev zlib1g-dev  libreadline-dev libsystemd-dev



# 2.编译安装lua环境
##下载源码:参考链接http://www.lua.org/start.html
[root@ka1 ~]$ curl -R -O http://www.lua.org/ftp/lua-5.4.4.tar.gz
[root@ka1 ~]$ tar xvf lua-5.3.5.tar.gz -C /usr/local/src
[root@ka1 ~]$ cd /usr/local/src/lua-5.3.5
[root@ka1 lua-5.3.5]$ make all test
[root@ka1 lua-5.3.5]$ pwd
/usr/local/src/lua-5.3.5
[root@ka1 lua-5.3.5]$ ./src/lua -v
Lua 5.3.5 Copyright (C) 1994-2018 Lua.org, PUC-Rio



# 3.编译安装haproxy
##下载源码:官网链接:www.haproxy.org
[root@ka1 ~]$ https://www.haproxy.org/download/2.6/src/haproxy-2.6.9.tar.gz
[root@ka1 ~]$ tar xvf haproxy-2.6.9.tar.gz -C /usr/local/src
[root@ka1 ~]$ cd /usr/local/src/haproxy-2.6.9

##编译安装
[root@ka1 haproxy-2.6.9]$ make ARCH=x86_64 TARGET=linux-glibc USE_PCRE=1 USE_OPENSSL=1 USE_ZLIB=1 USE_SYSTEMD=1 USE_CPU_AFFINITY=1 USE_LUA=1 LUA_INC=/usr/local/src/lua-5.3.5/src/ LUA_LIB=/usr/local/src/lua-5.3.5/src/ PREFIX=/apps/haproxy
[root@ka1 haproxy-2.6.9]$ make install PREFIX=/apps/haproxy

##解决环境变量
[root@ka1 haproxy-2.6.9]$ ln -s /apps/haproxy/sbin/haproxy /usr/sbin/

##验证haproxy版本
[root@ka1 haproxy-2.6.9]$ which haproxy 
/usr/sbin/haproxy
[root@ka1 haproxy-2.6.9]$ haproxy -v
HAProxy version 2.6.9-3a3700a 2023/02/14 - https://haproxy.org/
Status: long-term supported branch - will stop receiving fixes around Q2 2027.
Known bugs: http://www.haproxy.org/bugs/bugs-2.6.9.html
Running on: Linux 4.18.0-348.el8.0.2.x86_64 #1 SMP Sun Nov 14 00:51:12 UTC 2021 x86_64



# 4.创建HAProxy配置文件
[root@ka1 haproxy-2.6.9]$ cd
##准备配置文件目录
[root@ka1 ~]$ mkdir /etc/haproxy
[root@ka1 ~]$ cat > /etc/haproxy/haproxy.cfg <<EOF
global
    maxconn 100000
    stats socket /var/lib/haproxy/haproxy.sock mode 600 level admin
    uid 99    #指定运行haproxy的用户身份
    gid 99    #指定运行haproxy的用户身份
    daemon    #以守护进程运行
    nbthread 2  #指定每个haproxy进程开启的线程数,默认为每个进程一个线程
    cpu-map 1/all 0-1    ##haproxy2.4中启用nbthreads,在global配置中添加此选项,可以进行线程和CPU的绑定
    pidfile /var/lib/haproxy/haproxy.pid
    log 127.0.0.1 local3 info

defaults
    option http-keep-alive    #开启与客户端的会话保持
    option forwardfor         #透传客户端真实IP至后端web服务器
    maxconn 100000
    mode http				  #设置默认工作类型,使用TCP服务器性能更好,减少压力
    timeout connect 300000ms  #客户端请求从haproxy到后端server最长连接等待时间(TCP连接之前),默认单位ms
    timeout client 300000ms   #设置haproxy与客户端的最长非活动时间,默认单位ms,建议和timeoutserver相同 
    timeout server 300000ms   #客户端请求从haproxy到后端服务端的请求处理超时时长(TCP连接之后),默认单位ms,如果超时,会出现502错误,此值建议设置较大些,防止出现502错误

listen stats
    mode http					#http协议
    bind 10.0.0.8:9999          #对外发布的IP及端口。#指定HAProxy的监听地址,可以是IPV4或IPV6,可以同时监听多个IP或端口。在ka2主机上该项应该改为自己主机的IP地址
    stats enable
    log global
    stats uri /haproxy-status
    stats auth admin:123456
EOF

##准备socket文件目录
[root@ka1 ~]$ mkdir -p /var/lib/haproxy


# 5.创建用户及组
[root@ka1 ~]$ groupadd -g 99 haproxy
[root@ka1 ~]$ useradd -u 99 -g haproxy -d /var/lib/haproxy -M -r -s /sbin/nologin haproxy


# 6.创建服务启动service文件
[root@ka1 ~]$ cat > /lib/systemd/system/haproxy.service <<EOF
[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
EOF


# 7.使用子配置文件保存配置
## 当业务众多时,将所有配置都放在一个配置文件中,会造成维护困难。可以考虑按业务分类,将配置信息拆分,放在不同的子配置文件中,从而达到方便维护的目的。

##创建子配置目录
[root@ka1 ~]$ mkdir /etc/haproxy/conf.d
##添加子配置文件目录到service文件中
[root@ka1 ~]$ vi /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 -f /etc/haproxy/conf.d -c -q   #这一行添加-f 子配置文件目录
ExecStart=/usr/sbin/haproxy -Ws -f /etc/haproxy/haproxy.cfg -f /etc/haproxy/conf.d -p /var/lib/haproxy/haproxy.pid #这一行添加-f 子配置文件目录
ExecReload=/bin/kill -USR2

[Install]
WantedBy=multi-user.target


# 8.修改内核参数,让haproxy bind在非本机的ip上(也就是Keepalived产生的VIP),在keepalived从节点上,刚开始是没有这个IP的,如果不修改这个内核参数,启动不了haproxy服务
[root@ka1 ~]$ vi /etc/sysctl.conf
net.ipv4.ip_nonlocal_bind = 1 #添加这一行
#使内核参数生效
[root@ka1 ~]$ sysctl -p
net.ipv4.ip_nonlocal_bind = 1


# 9.启动服务
[root@ka1 ~]$ systemctl daemon-reload
[root@ka1 ~]$ systemctl enable --now haproxy
###现在就可以在子配置目录中根据不同的业务来创建对应的配置文件了。



# 10.ka2节点按照ka1节点安装操作从头做一遍
  1. 创建两个不同业务的配置文件
#ka1节点配置
##keepalived子配置文件
[root@ka1 ~]$ cd /etc/keepalived/conf.d/

##创建haproxy检测脚本
[root@ka1 ~]$ cat /etc/keepalived/conf.d/check_haproxy.sh
#!/bin/bash
/usr/bin/killall -0 haproxy || systemctl restart haproxy
##添加执行权限
[root@ka1 ~]$ chmod a+x /etc/keepalived/conf.d/check_haproxy.sh

##创建邮件通知脚本
[root@ka1 ~]$ cat /etc/keepalived/notify.sh 
#!/bin/bash
contact='lgq6579@163.com'
email_send='1499214187@qq.com'
email_passwd='zzvjrqnkrkafbaec'
email_smtp_server='smtp.qq.com'

. /etc/os-release

msg_error() {
    echo -e "\033[1;31m$1\033[0m"
}

msg_info() {
    echo -e "\033[1;32m$1\033[0m"
}

msg_warn() {
echo -e "\033[1;33m$1\033[0m"
}

color () {
    RES_COL=60
    MOVE_TO_COL="echo -en \\033[${RES_COL}G"
    SETCOLOR_SUCCESS="echo -en \\033[1;32m"
    SETCOLOR_FAILURE="echo -en \\033[1;31m"
    SETCOLOR_WARNING="echo -en \\033[1;33m"
    SETCOLOR_NORMAL="echo -en \E[0m"
    echo -n "$1" && $MOVE_TO_COL
    echo -n "["
    if [ $2 = "success" -o $2 = "0" ] ;then
        ${SETCOLOR_SUCCESS}
        echo -n $" OK "
    elif [ $2 = "failure" -o $2 = "1" ] ;then
        ${SETCOLOR_FAILURE}
        echo -n $"FAILED"
    else
        ${SETCOLOR_WARNING}
        echo -n $"WARNING"
    fi
    ${SETCOLOR_NORMAL}
    echo -n "]"
    echo
}

install_sendemail () {
    if [[ $ID =~ rhel|centos|rocky ]];then
        rpm -q sendemail &> /dev/null || yum -y install sendemail
    elif [ $ID = 'ubuntu' ];then
        dpkg -l | grep -q sendemail || { apt update; apt -y install libio-socket-ssl-perl libnet-ssleay-perl sendemail; }
    else
        color "不支持此操作系统,退出!" 1
        exit
    fi
}

send_mail() {
    local email_receive="$1"
    local email_subject="$2"
    local email_message="$3"
    sendemail -f $email_send -t $email_receive -u $email_subject -m $email_message -s $email_smtp_server -o message-charset=utf-8 -o tls=yes -xu $email_send -xp $email_passwd
    [ $? -eq 0 ] && color "邮件发送成功" 0 || color "邮件发送失败" 1
}

notify() {
    if [[ $1 =~ ^(master|backup|fault)$ ]];then
        mailsubject="$(hostname) to be $1, vip floating"
        mailbody="$(date +'%F %T'): vrrp transition, $(hostname) changed to be $1"
        send_mail "$contact" "$mailsubject" "$mailbody"
    else
        echo "Usage: $(basename $0) {master|backup|fault}"
        exit 1
    fi
}

install_sendemail
notify $1
##加执行权限
[root@ka1 ~]$ chmod a+x /etc/keepalived/notify.sh


##创建子配置文件
[root@ka1 conf.d]$ cat web.conf 
vrrp_script check_haproxy {
    script "/etc/keepalived/conf.d/check_haproxy.sh"  ##shell命令或脚本路径
    interval 1			#间隔时间,单位为秒,默认1秒
    weight -30			#默认为0,如果设置此值为负数,当上面脚本返回值为非0时,会将此值与本节点权重相加可以降低本节点权重,即表示fall. 如果是正数,当脚本返回值为0,会将此值与本节点权重相加可以提高本节点权重,即表示 rise.通常使用负值
    fall 3				#执行脚本连续几次都失败,则转换为失败,建议设为2以上
    rise 2				#执行脚本连续几次都成功,把服务器从失败标记为成功
    timeout 2			#超时时间
}

vrrp_instance VI_1 {
    state MASTER			#当前节点在此虚拟路由器上的初始状态,状态为MASTER,在ka2主机上要设置为BACKUP
    interface eth0     		#绑定为当前虚拟路由器使用的物理接口
    virtual_router_id 51	#每个虚拟路由器唯一标识,范围:0-255,每个虚拟路由器此值必须唯一,否则服务无法启动,同属一个虚拟路由器的多个keepalived节点必须相同,务必要确认在同一网络中此值必须唯一
    priority 100			#当前物理节点在此虚拟路由器的优先级,范围:1-254,每个keepalived主机节点此值不同。ka2主机要设置为80			
    advert_int 1			#vrrp通告的时间间隔,默认1s
    authentication {		#认证机制
        auth_type PASS
        auth_pass 6666661
    }
    virtual_ipaddress {		#虚拟IP,生产环境可能指定上百个IP地址
        10.0.0.200 dev eth0 label eth0:1  #指定VIP的网卡label
    }
    unicast_src_ip 10.0.0.8 	#指定发送单播的源IP
    unicast_peer{
        10.0.0.18				#指定接收单播的对方目标主机IP
    }
    notify_master "/etc/keepalived/notify.sh master"  	#当前节点成为主节点时触发的脚本
    notify_backup "/etc/keepalived/notify.sh backup"	#当前节点转为备节点时触发的脚本
    notify_fault "/etc/keepalived/notify.sh fault"		#当前节点转为“失败”状态时触发的脚本
    track_script {
        check_haproxy    #调用脚本
    }
}


##创建haproxy业务子配置文件
## 注意: 子配置文件的文件后缀必须为.cfg
[root@ka1 ~]$ cd /etc/haproxy/conf.d/
[root@ka1 conf.d]$ cat web.cfg 
frontend http_80
    bind 10.0.0.200:80
    acl org_domain hdr_dom(host) -i www.yanlinux.org
    acl edu_domain hdr_dom(host) -i www.yanlinux.edu
    use_backend www.yanlinux.org if org_domain
    use_backend www.yanlinux.edu if edu_domain
    
backend www.yanlinux.org
    server 10.0.0.28 10.0.0.28:80 check inter 3000 fall 3 rise 5

backend www.yanlinux.edu
    server 10.0.0.38 10.0.0.38:80 check inter 3000 fall 3 rise 5

##重启服务
[root@ka1 ~]$ systemctl restart keepalived.service haproxy.service


#ka2节点配置
##keepalived业务子配置文件
##从ka1节点上拷贝邮件通知脚本和haproxy检查脚本到本机上
[root@ka2 ~]$ scp 10.0.0.8:/etc/keepalived/notify.sh /etc/keepalived/
[root@ka2 ~]$ scp 10.0.0.8:/etc/keepalived/conf.d/check_haproxy.sh /etc/keepalived/conf.d/

##创建子配置文件,大致上与ka1节点上的配置相同
[root@ka2 ~]$ cat /etc/keepalived/conf.d/web.conf
vrrp_script check_haproxy {
    script "/etc/keepalived/conf.d/check_haproxy.sh"
    interval 1
    weight -30
    fall 3
    rise 2
    timeout 2
}

vrrp_instance VI_1 {
    state BACKUP         #这里改为BACKUP
    interface eth0
    virtual_router_id 51
    priority 80			 #改为80,因为是从节点
    advert_int 1
    authentication {
        auth_type PASS
        auth_pass 6666661
    }
    virtual_ipaddress {
        10.0.0.200 dev eth0 label eth0:1 
    }
    unicast_src_ip 10.0.0.18
    unicast_peer {
        10.0.0.8
    }
    notify_master "/etc/keepalived/notify.sh master"
    notify_backup "/etc/keepalived/notify.sh backup"
    notify_fault "/etc/keepalived/notify.sh fault"
    track_script {
        check_haproxy
    }
}

##创建haproxy业务子配置文件,直接从ka1节点拷贝过来就行
[root@ka2 ~]$ scp 10.0.0.8:/etc/haproxy/conf.d/web.cfg /etc/haproxy/conf.d

##重启服务
[root@ka2 ~]$ systemctl restart keepalived.service haproxy.service

两台keepalived状态页信息,测试keepalived业务搭建成功

9 整体架构完成,业务访问测试

[root@internet ~]$ curl -I www.yanlinux.org
HTTP/1.1 200 OK
server: nginx
date: Fri, 10 Mar 2023 06:36:29 GMT
content-type: text/html; charset=utf-8
set-cookie: KOD_SESSION_ID=aae53db9278d6386198b98a7a0441608; expires=Fri, 10-Mar-2023 10:36:29 GMT; Max-Age=14400; path=/; HttpOnly
set-cookie: CSRF_TOKEN=FGJc4urT5PVxmrWT; expires=Fri, 17-Mar-2023 06:36:29 GMT; Max-Age=604800; path=/

[root@internet ~]$ curl -I www.yanlinux.edu
HTTP/1.1 200 
server: nginx/1.22.1
date: Fri, 10 Mar 2023 06:38:27 GMT
content-type: text/html;charset=UTF-8
set-cookie: csrf_token=c871c9a8e1e34c38a7773ad96cea0f09; Path=/

网页浏览器查看