2020年6月

本文采用知识共享 署名-相同方式共享 4.0 国际 许可协议进行许可。
访问 https://creativecommons.org/licenses/by-sa/4.0/ 查看该许可协议。

1) LVS D-NAT 模型

发生 404 了

2) LVS DR 模型(多公网 IP)

发生 404 了

2.1) DR 模型搭建

2.1.0) 前言

DR 模型的关键在于,如何让 RealServer 持有 VIP,但是不向本机外通告持有该 VIP。
实现需要修改内核参数 arp_ignore、arp_announce,并在环回接口上虚拟 VIP。
将 arp_ignore 设置 1,不响应 VIP 的 arp 广播请求; arp_announce 设置 2,不向外通告拥有 VIP。

2.1.0.1) arp_ignore

0:只要本地配置有相应地址,就给予相应。
1:仅在请求到达的接口 MAC 地址匹配时,才给予响应。

2.1.0.2) arp_announce

0:将本地任何接口上任何地址向外通告。
1:试图仅向目标网络通告与其网络匹配的地址。
2:仅向与本地接口上地址匹配的网络进行通告。

2.1.1) 服务端机器 lvs-web-1 (10.9.6.11)

# 软件包准备并搭建一个 web 容器
yum install net-tools httpd -y
echo web-1 > /var/www/html/index.html
systemctl start httpd
systemctl enable httpd
# (注意,将 enp0s5 替换成相应的网卡)
echo 'net.ipv4.conf.enp0s5.arp_ignore = 1' | tee -a /etc/sysctl.conf
echo 'net.ipv4.conf.enp0s5.arp_announce = 2' | tee -a /etc/sysctl.conf
echo 'net.ipv4.conf.all.arp_ignore = 1' | tee -a /etc/sysctl.conf
echo 'net.ipv4.conf.all.arp_announce = 2' | tee -a /etc/sysctl.conf
sysctl -p
# 配置 VIP 到环回接口上
ifconfig lo:0 10.9.6.10 netmask 255.255.255.255

2.1.2) 服务端机器 lvs-web-2 (10.9.6.12)

# 软件包准备并搭建一个 web 容器
yum install net-tools httpd -y
echo web-2 > /var/www/html/index.html
systemctl start httpd
systemctl enable httpd
# (注意,将 enp0s5 替换成相应的网卡)
echo 'net.ipv4.conf.enp0s5.arp_ignore = 1' | tee -a /etc/sysctl.conf
echo 'net.ipv4.conf.enp0s5.arp_announce = 2' | tee -a /etc/sysctl.conf
echo 'net.ipv4.conf.all.arp_ignore = 1' | tee -a /etc/sysctl.conf
echo 'net.ipv4.conf.all.arp_announce = 2' | tee -a /etc/sysctl.conf
sysctl -p
# 配置 VIP 到环回接口上
ifconfig lo:0 10.9.6.10 netmask 255.255.255.255

2.1.3) 负载机器 lvs-main (10.9.6.3/10.9.6.10)

ipvsadm -A -t 10.9.6.10:80 -s rr
ipvsadm -a -t 10.9.6.10:80 -r 10.9.6.12 -g -w 1
ipvsadm -a -t 10.9.6.10:80 -r 10.9.6.11 -g -w 1

3) LVS DR 模型(单公网 IP)

发生 404 了

本文采用知识共享 署名-相同方式共享 4.0 国际 许可协议进行许可。
访问 https://creativecommons.org/licenses/by-sa/4.0/ 查看该许可协议。

1) OSI 七层参考模型

  1. 应用层
  2. 表示层
  3. 会话层
  4. 传输控制层
  5. 网络层
  6. 链路层
  7. 物理层

2) TCP/IP 四层精简模型

2.1) 应用层 (用户界面)

2.2) 传输层 (Port)

三次握手 -> 数据传输 -> 四次分手

2.3) 网络层 (IP)

基于路由表实现,Linux 查看路由表 route -n,基础路由表来源是 OS 中的网卡配置。
网络层就是找到下一跳,从上往下基于路由表每一条做匹配:目标 IP 与一条路由的掩码做与运算,与该条目 Destination 匹配则找了下一跳。

2.4) 链路层 (MAC)

链路层基于 MAC 地址,将含有目标 IP 的数据包再套上一层,链路层也有一张路由表,Linux 查看路由表 `arp -a
arp 会解释 IP 和网卡 MAC 地址的映射,受限与局域网,将数据包丢给下一个节点(下一跳、下一个 MAC 地址)

2.4.1) ARP 路由表来源

当机器刚开机时,ARP 表是空的,只有下一跳的 IP,那么数据包应该怎么发出去呢?即本机 ARP 缓存表中没有目标地址的缓存,将会经历一个 ARP 解析流程:

  1. 打包一个包含(源 IP、源 MAC、目标 IP、目标MAC 为 FF:FF:FF:FF:FF:FF) 的包,交换机收到 MAC 地址为 FF:FF:FF:FF:FF:FF 的包时会将此包广播给其他所有 Port
  2. 的目标机器收到包,若机器 IP 不匹配下一跳 IP,此包将被丢弃
  3. 若 IP 匹配,则向源 MAC 回包,包含匹配目标机器的 MAC
  4. 源机器将此记录添入 ARP 表
2.4.1.1) ARP 欺骗

这里会引入一个 ARP 欺骗的问题,若同时有两台机器向源机器回 ARP Response,源机器最终会记录最后一个收到的 ARP Response,这就引发了 ARP 欺骗

2.5) TCP/IP 总结

从物理层开始,每一层都向上层提供服务访问点。
在连接因特网的普通微机上:

  • 物理层:服务访问点就是网卡接口 (RJ45接口或BNC接口)
  • 数据链路层:服务访问点是 MAC 地址
  • 网络层的服务访问点是 IP 地址
  • 传输层的服务访问点是 Port 端口号
  • 应用层提供的服务访问点是用户界面

3) Linux 命令行打开网站(不使用 CURL)

# 进入当前进程的文件描述符目录,0(stdin),1(stdout)和2(stderr)
cd /proc/$$/fd
# 创建一个 TCP Socket 输入输出链接文件 8,链接到百度
exec 8<> /dev/tcp/www.baidu.com/80
# 管道加 & 的意思代表 8 时文件描述符非文件
echo -e 'GET / HTTP/1.0\n' >& 8
# 将 8 的输出管道到 stdin
cat 0 <& 8

9) 参见

本文采用知识共享 署名-相同方式共享 4.0 国际 许可协议进行许可。
访问 https://creativecommons.org/licenses/by-sa/4.0/ 查看该许可协议。

1) 线程通信

题目:实现一个容器,提供两个方法,add / size,写两个线程,线程 1 添加 10 个元素到容器中,线程 2 实现监控元素的个数,当个数到 5 个时,线程 2 给出提示并结束。

1.1) 思路:

首先会面临两个问题,add / size 函数的一致性问题,和容器的线程见可见性问题

  • 容器选择:两种选择方案,使用普通 list,给 add / size 上锁;或者用 Collections 来生成同步容器
  • 可见性问题:遇到可见性问题第一个会想到 volatile,但是 volatile 不能保证元素内部发生改变时的线程间可见性问题。处理方式多种:notify、CountDownLatch、LockSupport 等等,Semaphore 都行。

LockSupport 版,代码少哈哈哈哈哈哈哈哈

public class Thread_Notify {

    List<Object> list = Collections.synchronizedList(new ArrayList<>());
    static Thread a;
    static Thread b;

    public void add(Object o) {
        list.add(o);
    }

    public int size() {
        return list.size();
    }

    public static void main(String[] args) {
        Thread_Notify instance = new Thread_Notify();

        a = new Thread(() -> {
        LockSupport.park();
        System.out.println("=====5");
        LockSupport.unpark(a);
        });

        b = new Thread(() -> {
        for (int i = 1; i <= 10; i++) {
            instance.add(i);
            System.out.println(i);
            if (5 == i) {
            LockSupport.unpark(b);
            LockSupport.park();
            }
        }
        });

        a.start();
        b.start();
    }
}

2) 线程阻塞

题目:写一个固定容量同步容器,拥有 put 和 get 方法,以及 getCount 方法,能够支持 2 个生产者线程和 10 个消费者线程的阻塞调用。

2.1) 思路

这题本质上是考知识面,是否知道 Condition,可以给容器加个泛型装逼 -。-

public class ThreadCondition {

    private final int MAX_SIZE = 10;
    private final LinkedList<Object> list = new LinkedList<>();
    private int count = 0;

    private Lock lock = new ReentrantLock();
    Condition producer = lock.newCondition();
    Condition consumer = lock.newCondition();

    public void put(Object o) {
        try {
        lock.lock();
        while (MAX_SIZE == list.size()) producer.await();

        list.add(o);
        ++count;
        consumer.signalAll();
        } catch (InterruptedException e) {
        e.printStackTrace();
        } finally {
        lock.unlock();
        }
    }

    public Object get() {
        Object result = null;
        try {
        lock.lock();
        while (0 == count) consumer.await();

        result = list.removeFirst();
        --count;
        producer.signalAll();
        } catch (InterruptedException e) {
        e.printStackTrace();
        } finally {
        lock.unlock();
        }
        return result;
    }

    public static void main(String[] args) {
        ThreadCondition list = new ThreadCondition();

        for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            for (int j = 0; j < 5; j++) {
            System.out.println(list.get());
            }
        }).start();
        }

        for (int i = 0; i < 2; i++) {
        new Thread(() -> {
            for (int j = 0; j < 25; j++) {
            list.put(Thread.currentThread().getName() + " - " + j);
            }
        }, "Producer " + i).start();
        }
    }
}

本文采用知识共享 署名-相同方式共享 4.0 国际 许可协议进行许可。
访问 https://creativecommons.org/licenses/by-sa/4.0/ 查看该许可协议。

理论上来说,Synchronized 是自动锁,Java 中还有一些手动锁,即本文主题,介绍一些常用锁。
手动锁要注意写在 finally 中,避免死锁发生。

锁种类大致有乐观锁、悲观锁、自旋锁、读写锁、(排它锁、共享锁、)分段锁等等

1) ReentrantLock (重入锁)

众所周知 Synchronized 是可重入的,有一个可以替代 Synchronized 的锁即 ReentrantLock,且提供更多 Synchronized 无法实现的功能。

重入即:有 AB 两方法,需使用同一把锁,线程嵌套调用两方法是否会死锁。

1.1) 常用 API

  1. ReentrantLock(boolean fair):是否 fair,队列确保公平,默认非公平
  2. lock/unlock: 获取锁 / 解锁
  3. boolean trylock:尝试获取锁(可指定 try 的时间),返回是否成功
  4. lockInterruptibly:获取锁,可对 interrupt 响应
     Thread interrupt = new Thread(() -> {
         try {
             lock.lockInterruptibly(); // 获取锁
         } catch (InterruptedException e) {
             // ...... 收到 interrupt 信号处理
         } finally {
             // 要注意没拿到锁 unlock 会抛异常
             if (lock.isHeldByCurrentThread()) lock.unlock();
         }
     });
     interrupt.start();
     interrupt.interrupt();

1.2) 对比 Synchronized 优点

  • 基于 CAS
  • trylock 尝试获取锁
  • lockInterruptibly 可打断
  • fair 公平锁

2) CountDownLatch (倒计数门闩)

等待计数器归零,再继续运行程序

  1. CountDownLatch(count)
  2. await:线程进入阻塞,等待倒计数结束,可选指定超时时间
  3. countDown:count 计数减一
  4. getCount

3) CyclicBarrier (循环屏障)

循环设置一个线程数屏障,当线程数达到屏障数时,放行这些线程

  1. CyclicBarrier:设置屏障,指定屏障计数,可选指定每次打开屏障执行一个线程。
  2. await: 线程进入阻塞,等待屏障打开,可指定超时时间
  3. getNumberWaiting:正在等待线程数
  4. getParties:获取构造方法计数数量
  5. isBroken
  6. reset

4) Phase

大致是 CountDownLatch 和 CyclicBarrier 的整合, 可用来实现遗传算法,重写 Phase 自定义每个阶段操作

5) ReadWriteLock、StampedLock

内部有两把锁,读锁共享锁,写锁互斥锁。
读写互斥(排他),读锁被使用时其他读线程可共享这把锁
支持 Condition

  • ReentrantReadWriteLock(fair):是否公平,默认不公平
  • boolean trylock:尝试获取锁(可指定 try 的时间),返回是否成功
  • lockInterruptibly:获取锁,可对 interrupt 响应
  • newCondition

StampedLock 是 ReadWriteLock 的升级态,这里就不贴 API 了可以对照着使用。

6) Semaphore (信号量)

限制同时运行的线程数,可用于限流

  • Semaphore: 可指定是否公平
  • acquire / acquireUnInterraptibly:获取许可,为获取到许可则等待,默认为可 interrapter
  • release: 释放许可
  • tryAcquire:尝试获取许可,可指定超时

7) Exchanger

用于两个线程间通信交换变量,例:游戏交换装备

  • Exchanger
  • exchange(obj):线程进入等待,当有两个线程同时调用了 exchange,交换这两个线程传入对象的引用

8) LockSupport

线程阻塞工具,可以在线程内任意位置阻塞,常用 API 即 park / unpark 阻塞线程和唤醒线程

本文采用知识共享 署名-相同方式共享 4.0 国际 许可协议进行许可。
访问 https://creativecommons.org/licenses/by-sa/4.0/ 查看该许可协议。

java.util.concurent 包下有一个 atomic,作用如其名,atomic 包下的类可以解决原子性问题。

1) Atomic

juc 提供了一些原子性的工具,可以实现对象的原子性,如 AtomicInteger、AtomicLong 可以实现原子的 Integer 和 Long。

1.1) 原理

原理是基于 Java 的下层 API:Unsafe 的 CAS(Compare and swap)保证原子性,和 volatile 保证线程可见性。
CAS(无锁优化、自旋、乐观锁) 就是一个自旋,不断的比较目标实例是否符合预期原值,符合才将目标值 swap 到目标实例。

CAS 存在 ABA 问题:我们想将 A swap 成 C,但是在 CAS 过程中,A 的值发生过改变但最后又变回了 A,compare 成功,CAS
,这就是 ABA 问题,一般情况下不影响业务逻辑,可以通过 versionNum 来解决 ABA 问题,JUC 也提供了 AtomicStampedReference 解决 ABA 问题。

2) LongAdder/DoubleAdder

在 JDK8 后,atomic 包中新增了两个类,LongAdder、DoubleAdder
在线程高竞争情况下 Adder 的效率要更高,但是会带来更大的空间消耗,且没有提供 xxAndGet 这样的 API,所以算是不安全的 Read。

2.1) 原理

Adder 相关类继承了 Striped64,Striped64 内部维护了一个 Cell 数组,并发时会把每个线程所做的修改分散到一个个 Cell 中
,Cell 中通过 CAS 实现原子性。当需要取 value 时,会把 Cell 中数据整合在一起,从而在线程竞争激烈时提高效率。

3) Accumulator

XXAdder 只能实现递增,如果有其他的需求,可以使用 XXAccumulator 自定义函数来实现非递增操作

Title - Artist
0:00