2023年4月

背景

在微服务项目中,大家都会去使用到分布式锁,一般也是使用Redis去实现,使用RedisTemplate、Redisson、RedisLockRegistry都行,公司的项目中,使用的是Redisson,一般你会怎么用?看看下面的代码,是不是就是你的写法

String lockKey = "forlan_lock_" + serviceId;
RLock lock = redissonClient.getLock(lockKey);

// 方式1
try {
lock.lock(5, TimeUnit.SECONDS);
// 执行业务
...
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
}

// 方式2
try {
if (lock.tryLock(5, 5, TimeUnit.SECONDS)) {
// 获得锁执行业务
...
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
}


分析

像上面的写法,符合我们的常规思维,一般,为了避免程序挂了的情况,没有释放锁,都会设置一个过期时间
但这个过期时间,一般设置多长?

设置过短,会导致我们的业务还没有执行完,锁就释放了,其它线程拿到锁,重复执行业务
设置过长,如果程序挂了,需要等待比较长的时间,锁才释放,占用资源

这时候,你会说,一般我们可以根据业务执行情况,设置个过期时间即可,对于部分执行久的业务,Redisson内部是有个看门狗机制,会帮我们去续期,简单来说,就是有个定时器,会去看我们的业务执行完没,没有就帮我们进行延时,看似没有问题吧,那我们来简单看下源码,无论我们使用哪种方式,最终都会进到这个方法,就是看门狗机制的核心代码

private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
if (leaseTime != -1L) {
// 前面我们指定了过期时间,会进到这里,直接加锁
return this.tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
// 没有指定过期时间的话,默认采用LockWatchdogTimeout,默认是30s
RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
// ttlRemainingFuture执行完,添加一个监听器,类似netty的时间轮
ttlRemainingFuture.addListener(new FutureListener<Long>() {
public void operationComplete(Future<Long> future) throws Exception {
if (future.isSuccess()) {
Long ttlRemaining = (Long)future.getNow();
if (ttlRemaining == null) {
RedissonLock.this.scheduleExpirationRenewal(threadId);
}
}
}
});
return ttlRemainingFuture;
}

scheduleExpirationRenewal方法

private void scheduleExpirationRenewal(final long threadId) {
if (!expirationRenewalMap.containsKey(this.getEntryName())) {
Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
public void run(Timeout timeout) throws Exception {
// renewExpirationAsync就是执行续期的方法
RFuture<Boolean> future = RedissonLock.this.renewExpirationAsync(threadId);
// 什么时候触发执行?
future.addListener(new FutureListener<Boolean>() {
public void operationComplete(Future<Boolean> future) throws Exception {
RedissonLock.expirationRenewalMap.remove(RedissonLock.this.getEntryName());
if (!future.isSuccess()) {
RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", future.cause());
} else {
if ((Boolean)future.getNow()) {
RedissonLock.this.scheduleExpirationRenewal(threadId);
}

}
}
});
}
}, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS); // 当跑了LockWatchdogTimeout的1/3时间就会去执行续期
if (expirationRenewalMap.putIfAbsent(this.getEntryName(), new RedissonLock.ExpirationEntry(threadId, task)) != null) {
task.cancel();
}
}

所以,结论是啥?

// 方式1
lock.lock(5, TimeUnit.SECONDS);
// 方式2
lock.tryLock(5, 5, TimeUnit.SECONDS)

我们这两种写法都会导致看门狗机制失效,如果业务执行超过5s,就会出问题


解决

正确的写法应该是,不指定过期时间

// 方式1
lock.lock();
// 方式2
lock.tryLock(5, -1, TimeUnit.SECONDS)

你可以会觉得不妥,不指定的话,就默认按照30s续期时间,然后每10s去看看有没有执行完,没有就续期,
我们也可以指定续期时间,比如指定为15s

config.setLockWatchdogTimeout(15000L);


总结

  • 在使用Redisson实现分布式锁,不应该设置过期时间
  • 看门狗默认续期时间是30s,可以通过setLockWatchdogTimeout指定
  • 看门狗会每internalLockLeaseTime / 3L去续期
  • 看门狗底层实际就是类似Netty的时间轮

对于初学者来说,如何搭建FFmpeg的开发环境是个不小的拦路虎,因为FFmpeg用到了许多第三方开发包,所以要先编译这些第三方源码,之后才能给FFmpeg集成编译好的第三方库。
不过考虑到刚开始仅仅调用FFmpeg的API,不会马上去改FFmpeg的源码,因此只要给系统安装编译好的FFmpeg动态库,即可着手编写简单的FFmpeg程序。比如这个网站
https://github.com/BtbN/FFmpeg-Builds/releases
提供了已经编译通过的FFmpeg开发包,囊括Linux、Windows等系统环境的开发版本。对该网站提供的Linux版FFmpeg安装包而言,需要事先安装不低于2.22版本的glibc库,否则编译FFmpeg程序会报错“undefined reference to `_ZGVdN4vv_pow@GLIBC_2.22'”。下面介绍在Linux系统安装已编译的FFmpeg详细步骤。

一、安装glibc

1、到这个网址下载2.23版本的glibc源码包http://ftp.gnu.org/gnu/glibc/。注意:虽然要求glibc版本不低于2.22,但是不宜安装过高版本的glibc,因为较高版本的glibc依赖于python,去整python环境又得费一番功夫,所以弄个比2.22稍高一点的2.23版就够了,也就是下载这个压缩包
http://ftp.gnu.org/gnu/glibc/glibc-2.23.tar.gz

2、先解压glibc源码包,再进入glibc源码目录,然后创建build目录并进入该目录,也就是依次执行以下命令:

tar zxvf glibc-2.23.tar.gz
cd glibc-2.23
mkdir build
cd build

3、在build目录下依次执行以下命令配置、编译与安装glibc:

../configure --prefix=/usr
make
make install

安装成功后,会在/usr/lib64目录下找到最新的libc.so(还有libc.so.6和libc-2.23.so)和libmvec.so(还有libmvec.so.1和libmvec-2.23.so)等库文件。

二、安装FFmpeg

1、到这个网址下载Linux环境编译好的FFmpeg安装包
https://github.com/BtbN/FFmpeg-Builds/releases
,比如ffmpeg-master-latest-linux64-gpl-shared.tar.xz。
2、把下载好的FFmpeg安装包解压到/usr/local/ffmpeg目录,也就是依次执行以下命令:

cd /usr/local
tar xvf ffmpeg-master-latest-linux64-gpl-shared.tar.xz
mv ffmpeg-master-latest-linux64-gpl-shared ffmpeg

3、输入cd命令回到当前用户的初始目录,使用vi打开该目录下的.bash_profile,也就是依次执行以下命令:

cd
vi .bash_profile

4、把光标移动到文件末尾,按下a键进入编辑模式,然后在文件末尾添加下面四行环境变量配置:

PATH=$PATH:/usr/local/ffmpeg/bin
export PATH
LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/ffmpeg/lib
export LD_LIBRARY_PATH

接着保存并退出文件,也就是先按下Esc键退出编辑模式,再按下冒号键,接着输入wq再按回车键,即可完成修改操作。

5、执行以下命令加载最新的环境变量:

source .bash_profile

接着运行下面的环境变量查看命令:

env | grep PATH

发现控制台回显的PATH串包含/usr/local/ffmpeg/bin,同时LD_LIBRARY_PATH串包含/usr/local/ffmpeg/lib,说明FFmpeg的bin目录和lib目录都加载进了环境变量。

三、编写测试程序

1、创建C代码文件名叫hello.c,填入下面的代码内容:

#include <libavutil/avutil.h>

int main(int argc, char* argv[]) {
    av_log(NULL, AV_LOG_INFO, "hello world\n");
}

2、保存并退出该文件,执行以下命令编译hello.c:

gcc hello.c -o hello -I/usr/local/ffmpeg/include -L/usr/local/ffmpeg/lib -lavformat -lavdevice -lavfilter -lavcodec -lavutil -lswscale -lswresample -lpostproc -lm

3、运行编译好的hello程序,也就是执行以下命令:

./hello

发现控制台回显日志信息“hello world”,表示测试程序运行正常,说明FFmpeg开发环境已经成功搭建。

4、刚才的测试程序hello.c采用C语言编写,并且使用gcc编译。若要采用C++编程的话,则需改成下面的hello.cpp代码:

#include <iostream>

// 因为FFmpeg源码使用C语言编写,所以在C++代码中调用FFmpeg的话,要使用标记“extern "C"{……}”把FFmpeg的头文件包含进来
extern "C"
{
#include <libavutil/avutil.h>
}

int main(int argc, char* argv[]) {
    av_log(NULL, AV_LOG_INFO, "hello world\n");
}

鉴于C++代码采用g++编译,于是hello.cpp的编译命令变成下面这样:

g++ hello.cpp -o hello -I/usr/local/ffmpeg/include -L/usr/local/ffmpeg/bin -lavformat -lavdevice -lavfilter -lavcodec -lavutil -lswscale -lswresample -lpostproc -lm

编译完毕,同样生成名叫hello的可执行程序,如此就实现了C++代码集成FFmpeg函数的目标了。

前言

IP冲突引起的网络异常,可以通过检查IP是否冲突,排除故障。我们可以用一些工具进行检查,例如arp-scan、arping软件进行查看。

这里使用arping进行检查设备的MAC地址,通过查查看MAC地址是否唯一,从而判断IP是否冲突,

原理:每台设备的MAC地址是唯一的,若arping返回的MAC出现2个甚至多个,说明这个IP对应于多台设备,则存在IP地址冲突的情况。

作者:良知犹存

转载授权以及围观:欢迎关注微信公众号:
羽林君

或者添加作者个人微信:
become_me


arping介绍:

arping命令来自于英文词组”ARP ping“的缩写,其功能是用于发送ARP请求报文,ARP全称为”Address Resolution Protocol“,中文译为地址解析协议。arping命令是以广播地址发送arp packets,以太网内所有的主机都会收到这个arp packets,但是本机收到之后不会Reply任何信息,来测试网络状态,能够判断某个指定IP地址是否在网络上已被使用,并能够获取更多设备信息,像是加强版的ping命令。

openwrt编译:

make menuconfig进入,“ \ ” 进行搜索arping,查看编译具体的配置

搜索情况如下:

Symbol: BUSYBOX_CONFIG_ARPING [=n]                                                                                                    
Type  : bool                                                                                                                      
Defined at package/utils/busybox/config/networking/Config.in:92                                                                   
  Prompt: arping (9 kb)                                                                                                           
  Depends on: (PACKAGE_busybox [=y] || PACKAGE_busybox-selinux [=n]) && BUSYBOX_CUSTOM [=n]                                       
  Location:                                                                                                                       
 (1) -> Base system                                                                                                                
       -> Networking Utilities                                                                                                     
                                                                                                                                   
                                                                                                                                   
Symbol: BUSYBOX_CONFIG_FEATURE_UDHCPC_ARPING [=n]                                                                                 
Type  : bool                                                                                                                      
Defined at package/utils/busybox/config/networking/udhcp/Config.in:72                                                             
  Prompt: Verify that the offered address is free, using ARP ping                                                                 
  Depends on: (PACKAGE_busybox [=y] || PACKAGE_busybox-selinux [=n]) && BUSYBOX_CUSTOM [=n] && BUSYBOX_CONFIG_UDHCPC [=n]         
  Location:                                                                                                                       
 (2) -> Base system                                                                                                                
       -> Networking Utilities                                                                                        
         -> udhcpc (24 kb) (BUSYBOX_CONFIG_UDHCPC [=n])

Symbol: BUSYBOX_DEFAULT_ARPING [=n]                                                                                
Type  : bool                                                                                                         
Defined at package/utils/busybox/Config-defaults.in:2241                                                             
  Depends on: PACKAGE_busybox [=y] || PACKAGE_busybox-selinux [=n]                                                   
                                                                                                                      
                                                                                                                      
Symbol: BUSYBOX_DEFAULT_FEATURE_UDHCPC_ARPING [=n]                                                                   
Type  : bool                                                                                                         
Defined at package/utils/busybox/Config-defaults.in:2676                                                             
  Depends on: PACKAGE_busybox [=y] || PACKAGE_busybox-selinux [=n]

第一个就是
Prompt: arping (9 kb)
,直接开始设置,进入
Base system

设置
Customize busybox options
为y打开,这个时候就可以后续的
Networking Utilities
配置


找到
Networking Utilities

里面找一下
arping
设置y

make
编译

编译固件进行替换升级
sysupgrade -n op openwrt-ramips-mt7621-xiaomi_redmi-router-ac2100-squashfs-sysupgrade.bin

重启后就可以看到arping这个软件

arping使用:

选项

-f:表示在收到第一个响应报文后就退出;
-q:quiet output不显示任何信息;
-b:用于发送以太网广播帧(FFFFFFFFFFFF)。arping一开始使用广播地址,在收到响应后就使用unicast地址。
-D:检测某个IP是否被使用,后边跟上一个IP地址
-U:主动的ARP模式,更新邻居的arp表
-A:ARP回复模式,更新邻居arp
-c N:发送数据包的数目
-w timeout:设定一个超时时间,单位是秒。如果到了指定时间,arping还没到完全收到响应则退出;
-I IFACE:指定使用的以太网设备,默认使用eth0
-s SRC_IP:指定源IP地址
DST_IP:指定目标IP地址

arping -I br-lan -c 3 192.168.1.151

我们可以查看mac地址看是否有重复ip的设备

同样类似使用wireshark捕获数据也可以看到 执行如下命令:
ssh -o HostKeyAlgorithms=+ssh-rsa -o PubkeyAcceptedKeyTypes=+ssh-rsa root@192.168.1.1 'tcpdump -s 0 -U -n -w - -i br-lan not port 22' | ./wireshark.exe -k -i -
这个时候也可以看到arping三次的过程

附录: 一个打印MAC地址的脚本:
arping -I br-lan -c 3 192.168.1.151 | awk '/reply/ {macaddr_str=$5; mac=substr(macaddr_str,2,length(macaddr_str) - 2);print mac}'

结语

这就是我自己在openwrt使用arping操作的分享。如果大家有更好的想法,也欢迎大家加我好友交流分享哈。


作者:良知犹存,白天努力工作,晚上原创公号号主。公众号内容除了技术还有些人生感悟,一个认真输出内容的职场老司机,也是一个技术之外丰富生活的人,摄影、音乐 and 篮球。关注我,与我一起同行。

                              ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧  END  ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧

推荐阅读

【1】
jetson nano开发使用的基础详细分享

【2】
Linux开发coredump文件分析实战分享

【3】
CPU中的程序是怎么运行起来的
必读

【4】
cartographer环境建立以及建图测试

【5】
设计模式之简单工厂模式、工厂模式、抽象工厂模式的对比

本公众号全部原创干货已整理成一个目录,回复[ 资源 ]即可获得。

系列文章目录和关于我

零丶长轮询的引入

最近在看工作使用到的diamond配置中心原理,发现大多数配置中心在推和拉模型上做的选择出奇的一致选择了
基于长轮询的拉模型

  • 基于拉模型的客户端轮询的方案
    客户端通过轮询方式发现服务端的配置变更事件。轮询的频率决定了动态配置获取的实时性。


    • 优点:简单、可靠。
    • 缺点:应用增多时,较高的轮询频率给整个配置中心服务带来巨大的压力。

    另外,从配置中心的应用场景上来看,是一种写少读多的系统,客户端大多数轮询请求都是没有意义的,因此这种方案不够高效。

  • 基于推模型的客户端长轮询的方案

    基于Http长轮询模型,实现了让客户端在没有发生动态配置变更的时候减少轮询。这样减少了无意义的轮询请求量,提高了轮询的效率;也降低了系统负载,提升了整个系统的资源利用率。

一丶何为长轮询

长轮询
本质上是原始轮询技术的一种更有效的形式。

它的出现是为了解决:向服务器发送重复请求会浪费资源,因为必须为每个新传入的请求建立连接,必须解析请求的 HTTP 头部,必须执行对新数据的查询,并且必须生成和交付响应(通常不提供新数据)然后必须关闭连接并清除所有资源。

  • 从tomcat服务器的角度就是客户端不停请求,每次都得解析报文封装成Request,Response对象,并且占用线程池中的一个线程。
  • 并且每次轮询都要进行tcp握手,挥手,网卡发起中断,操作系统处理中断从内核空间拷贝数据到用户空间,一通忙活服务端返回
    配置未修改(配置中心没有修改配置,客户端缓存的配置和配置中心一致,所以是白忙活)

长轮询是一种
服务器选择尽可能长的时间保持和客户端连接打开的技术

仅在数据变得可用或达到超时阙值后才提供响应

而不是在给到客户端的新数据可用之前,让每个客户端多次发起重复的请求

image-20230416133047669

简而言之,就是服务端并不是立马写回响应,而是hold住一段时间,如果这段时间有数据需要写回(例如配置的修改,新配置需要写回)再写回,然后浏览器再发送一个新请求,从而实现
及时性
,
节省网络开销
的作用。

image-20230416145221280

二丶使用等待唤醒机制写一个简单的“长轮询”(脱裤子放屁)

package com.cuzzz.springbootlearn.longpull;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.*;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

@RestController
@RequestMapping("long-pull")
public class MyController implements InitializingBean {

    /**
     * 处理任务的线程
     */
    private ThreadPoolExecutor processExecutor;
    /**
     * 等待唤醒的锁
     */
    private static final ReentrantLock lock = new ReentrantLock();
    /**
     * 当请求获取配置的时候,在此condition上等待一定时间
     * 当修改配置的时候通过这个condition 通知其他获取配置的线程
     */
    private static final Condition condition = lock.newCondition();

    @GetMapping
    public void get(HttpServletRequest request, HttpServletResponse response) throws ExecutionException, InterruptedException {
        //组转成任务
        Task<String> task = new Task<String>(request, response,
                () -> "拿配置" + System.currentTimeMillis());
        //提交到线程池
        Future<?> submit = processExecutor.submit(task);
        //tomcat线程阻塞于此
        submit.get();
    }

    /**
     * 模拟修改配置
     *
     * 唤醒其他获取配置的线程
     */
    @PostMapping
    public String post(HttpServletRequest request, HttpServletResponse response) {
        lock.lock();
        try {
            condition.signalAll();
        }finally {
            lock.unlock();
        }
        return "OK";
    }


    static class Task<T> implements Runnable {
        private HttpServletResponse response;
        /**
         * 等待时长
         */
        private final long timeout;
        private Callable<T> task;

        public Task(HttpServletRequest request, HttpServletResponse response, Callable<T> task) {
            this.response = response;

            String time = request.getHeader("time-out");
            if (time == null){
                //默认等待10秒
                this.timeout = 10;
            }else {
                this.timeout = Long.parseLong(time);
            }
            this.task = task;
        }


        @Override
        public void run() {
            lock.lock();
            try {

                //超市等待
                boolean await = condition.await(timeout, TimeUnit.SECONDS);
                //超时
                if (!await) {
                    throw new TimeoutException();
                }
                //获取配置
                T call = task.call();
                //写回
                ServletOutputStream outputStream = response.getOutputStream();
                outputStream.write(("没超时拿当前配置:" + call).getBytes(StandardCharsets.UTF_8));
            } catch (TimeoutException | InterruptedException exception) {
                //超时或者线程被中断
                try {
                    ServletOutputStream outputStream = response.getOutputStream();
                    T call = task.call();
                    outputStream.write(("超时or中断拿配置:" + call).getBytes(StandardCharsets.UTF_8));
                } catch (Exception ex) {
                    throw new RuntimeException(ex);
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }
        }
    }


    @Override
    public void afterPropertiesSet() {

        int cpuNums = Runtime.getRuntime().availableProcessors();

        processExecutor
                = new ThreadPoolExecutor(cpuNums, cpuNums * 2, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100), new ThreadPoolExecutor.CallerRunsPolicy());
    }
}

使用get方法反问的请求回被提交到线程池进行await等待,使用post方法的请求回唤醒这些线程。

但是这个写法有点脱裤子放屁

image-20230416143924564

为什么会出现这种情况,直接提交到线程池异步执行不可以么,加入我们删除上面
submit.get
方法会发现其实什么结果都不会,这是因为异步提交到线程池后,tomcat已经结束了这次请求,并没有维护这个连接,所以没有办法写回结果。

如果不删除这一行,tomcat线程阻塞住我们可以写回结果,但是其实没有达到配置使用长轮询的初衷——"解放tomcat线程,让配置中心服务端可以处理更多请求"。

image-20230416145058166

所以我们现在陷入一个尴尬的境地,怎么解决昵?看下去

三丶Tomcat Servlet 3.0长轮询原理

1.AsyncContext实现长轮询

package com.cuzzz.springbootlearn.longpull;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.AsyncContext;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

@RestController
@RequestMapping("long-pull3")
public class MyController2 {

    private static final ScheduledExecutorService procesExecutor
            = Executors.newSingleThreadScheduledExecutor();
    /**
     * 记录配置改变的map
     */
    private static final ConcurrentHashMap<String, String> configCache
            = new ConcurrentHashMap<>();
    /**
     * 记录长轮询的任务
     */
    private static final ConcurrentLinkedDeque<AsyncTask> interestQueue
            = new ConcurrentLinkedDeque<>();

    static {
        //每2秒看一下释放配置变更,或者任务超时
        procesExecutor.scheduleWithFixedDelay(() -> {
            List<AsyncTask>needRemove  = new ArrayList<>();
            for (AsyncTask asyncTask : interestQueue) {
                if (asyncTask.timeout()) {
                    asyncTask.run();
                    needRemove.add(asyncTask);
                    continue;
                }
                if (configCache.containsKey(asyncTask.configId)) {
                    needRemove.add(asyncTask);
                    asyncTask.run();
                }
            }
            interestQueue.removeAll(needRemove);
        }, 1, 2, TimeUnit.SECONDS);
    }


    static class AsyncTask implements Runnable {
        private final AsyncContext asyncContext;
        private final long timeout;
        private static long startTime;
        private String configId;

        AsyncTask(AsyncContext asyncContext) {
            this.asyncContext = asyncContext;
            HttpServletRequest request = (HttpServletRequest) asyncContext.getRequest();
            String timeStr = request.getHeader("time-out");
            if (timeStr == null) {
                timeout = 10;
            } else {
                timeout = Long.parseLong(timeStr);
            }
        	//关注的配置key,应该getParameter的,无所谓
            this.configId = request.getHeader("config-id");
            if (this.configId == null) {
                this.configId = "default";
            }
            
            //开始时间
            startTime = System.currentTimeMillis();
        }
		
        //是否超时
        public boolean timeout() {
            return (System.currentTimeMillis() - startTime) / 1000 > timeout;
        }

        @Override
        public void run() {
		
            String result = "开始于" + System.currentTimeMillis() + "--";
            try {
                if (timeout()) {
                    result = "超时: " + result;
                } else {
                    result += configCache.get(this.configId);
                }

                result += "--结束于:" + System.currentTimeMillis();
                ServletResponse response = asyncContext.getResponse();
                response.getOutputStream().write(result.getBytes(StandardCharsets.UTF_8));
                
                //后续将交给tomcat线程池处理,将给客户端响应
                asyncContext.complete();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }

        }

    }


    @GetMapping
    public void get(HttpServletRequest request, HttpServletResponse response) {
        //打印处理的tomcate线程id
        System.out.println("线程id" + Thread.currentThread().getId());
        //添加一个获取配置的异步任务
        interestQueue.add(new AsyncTask(asyncContext));
        //开启异步
        AsyncContext asyncContext = request.startAsync();
        asyncContext.setTimeout(0);
        //监听器打印最后回调的tomcat线程id
        asyncContext.addListener(new AsyncListener() {
            @Override
            public void onComplete(AsyncEvent event) throws IOException {
                System.out.println("线程id" + Thread.currentThread().getId());
            }
            //...剩余其他方法
        });
        
        //立马就会释放tomcat线程池资源
        System.out.println("tomcat主线程释放");
    }

    @PostMapping
    public void post(HttpServletRequest request) {
        String c = String.valueOf(request.getParameter("config-id"));
        if (c.equals("null")){
            c = "default";
        }
        String v = String.valueOf(request.getParameter("value"));
        configCache.put(c, v);
    }
}

image-20230416163659265

image-20230416164009702

上面演示利用
AsyncContext
tomcat是如何实现长轮询

这种方式的优势在于:解放了tomcat线程,其实tomcat的线程只是运行了get方法中的代码,然后立马可以去其他请求,真正
获取配置更改
的是我们的单线程定时2秒去轮询。

image-20230416170528139

2.实现原理

2.1 tomcat处理一个请求的流程

  • Connector是客户端连接到Tomcat容器的服务点,它提供协议服务来将引擎与客户端各种协议隔离开来

    在Connector组件中创建了Http11NioProtocol组件,Http11NioProtocol默认持有NioEndpoin,NioEndpoint中持有Acceptor和Poller,并且启动的时候会启动一个线程运行Acceptor

  • Acceptor服务器端监听客户端的连接,会启动线程一直执行

    image-20230416173102328

    每接收一个客户端连接就轮询一个Poller组件,添加到Poller组件的事件队列中。,每接收一个客户端连接就轮询一个Poller组件,添加到Poller组件的事件队列中。

  • Poller组件持有多路复用器selector,poller组件不停从自身的事件队列中将事件取出注册到自身的多路复用器上,同时多路复用器会不停的轮询检查是否有通道准备就绪,准备就绪的通道就可以扔给tomcat线程池处理了。

    image-20230416173513349

    image-20230416173724384

  • tomcat线程池处理请求


    • 这里会根据协议创建不同的Processor处理,这里创建的是Http11Processor,Http11Processor会使用CoyoteAdapter去解析报文随后交给Container去处理请求

    • CoyoteAdapter解析报文随后交给Container去处理请求

      image-20230416175001392

    • Container会将Filter和Servlet组装成FilterChain依次调用

      image-20230416180033573

    • FilterChain会依次调用Filter#doFilter,然后调用Servlet#service方法

      至此会调用到Servlete#service方法,SpringMVC中的Dispatcher会反射调用我们controller的方法

2.2 AsyncContext 如何实现异步

2.2.1 request.startAsync() 修改异步状态机状态为Starting

AsycContext内部持有一个AsyncStateMachine来管理异步请求的状态(有点状态模式的意思)

状态机的初始状态是AsyncState.DISPATCHED,通过setStarted将状态机的状态更新成STARTING

image-20230416181604623

2.2.2 AbstractProtocol启动定时任务处理超时异步请求

Connector启动的时候触发ProtocolHandler的start方法,如下

image-20230416182047418

其中startAsyncTimeout方法会遍历waitingProcessors中每一个Processor的timeoutAsync方法,这里的Processor就是Http11Processor

image-20230416182324701

那么waitProcessors中的Http11Processor是谁塞进去的昵?

tomcat线程在执行完我们的Servlet代码后,Http11NioProtocol会判断请求状态,如果为Long那么会塞到waitProcessors集合中。

如果发现请求超时,那么会调用
Http11Processor#doTimeoutAsycn
然后由封装的socket通道socketWrapper以TIMEOUT的事件类型重新提交到tomcat线程池中。

image-20230416183626153

2.2.3 AsyncContext#complete触发OPEN_READ事件

image-20230416184726098

可以看到其实和超时一样,只不过超时是由定时任务线程轮询来判断,而AsyncContext#complete则是我们业务线程触发processSocketEvent将后续处理提交到tomcat线程池中。

四丶长轮询的优点和缺点

本文学习了长轮询和tomcat长轮询的原理,可以看到这种方式的优点

  • 浏览器长轮询的过程中,请求并没有理解响应,而是等到超时或者有需要返回的数据(比如配置中心在这个超时事件内发送配置的变更)才返回,解决了短轮询频繁进行请求网络开销的问题,减少了读多写少业务情景下无意义请求。
  • 真是通过这种方式,减少了无意义的请求,而且释放了tomcat线程池中的线程,使得我们服务端可以支持更多的客户端(因为业务逻辑是放在其他的线程池执行的,而且对于配置中心来说,可以让多个客户端的长轮询请求由一个线程去处理,原本是一个请求一个tomcat线程处理,从而可以支持更多的请求)

当然这种方式也是有缺点的

  • hold住请求也是会消耗资源的,如果1w个请求同时到来,我们都需要hold住(封装成任务塞到队列)这写任务也是会占用内存的,而短轮询则会立马返回,从而时间资源的释放

  • 请求先后顺序无法保证,比如轮询第五个客户端的请求的时候,出现了配置的变更,这时候第五个请求会被提交到tomcat线程池中,从而早于前面四个请求得到响应,这对于需要严格有序的业务场景是有影响的

  • 多台实例监听配置中心实例,出现不一致的情况

    比如配置中心四台实例监听配置变更,前三台可能响应了得到V1的配置,但是轮询到第四台实例的请求的时候又发生了变更可能就得到了v2的配置,这时候这四台配置不一致了。需要保证这种一致性需要我们采取其他的策略,比如配置中心服务端主动udp推,或者加上版本号保证这四台配置一致。

ELF(Executable and Linkable Format) 即可执行可链接文件格式,是目前操作系统上最常见的可执行文件格式。不同系统的目标文件不一样,Windows是PE(Portable Executable),linux是ELF(Executable Linkable Format),它们都是COFF(Common file format)格式的变种。

1、基本格式

ELF格式的目标文件和可执行文件在结构上没有本质差异,ELF不仅仅描述目标文件,也用于描述可执行文件,Windows下的dll和.lib, Linux下的.so和.a文件都是按照类ELF格式存储,下图描述了ELF链接视图(.o文件、.so文件)和执行视图,链接视图描述了各个段(section)的组成,如.text、.data、bss段。执行视图由segment组成,segment用于表示一个一定长度的区域,按照只读/可读写划分,不区分数据的属性,如代码段、数据段。

目标文件是未经过链接的,里面的符号和地址没有调整导致无法运行。例如直接运行目标文件,系统提示无法执行该二进制文件。

$ . hello.o
bash: .: hello.o: cannot execute binary
file$filehello.o
hello.o: Intel amd64 COFF
object file, no line number info, not stripped, 7 sections, symbol offset=0x2a0, 22 symbols, 1st section name ".text"

ELF文件格式在字节对齐和元素解析时,与系统架构、字长有密切关系,ELF 文件由ELF header和各种段组成,其结构图如下所示。详细文档可以查阅
https://elinux.org/Executable_and_Linkable_Format_(ELF)

2、ELF文件头

ELF 文件的最前面是文件头,描述了ELF文件的基本属性,比如ELF文件版本、目标机器型号、程序入口地址。

详细的描述可以参考:
https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html

#define EI_NIDENT 16typedefstruct{
unsigned
chare_ident[EI_NIDENT];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry;
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
} Elf32_Ehdr;


$ readelf
-h /bin/ls
ELF Header:
Magic: 7f
45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00Class: ELF64
Data:
2's complement, little endian Version: 1(current)
OS
/ABI: UNIX -System V
ABI Version:
0Type: DYN (Position-Independent Executable file)
Machine: Advanced Micro Devices X86
-64Version:0x1Entry point address:0x6180Start of program headers:64(bytes into file)
Start of section headers:
145256(bytes into file)
Flags:
0x0Size ofthis header: 64(bytes)
Size of program headers:
56(bytes)
Number of program headers:
11Size of section headers:64(bytes)
Number of section headers:
30Section headerstring table index: 29

上面结构体里成员类型长度的定义为:

名称

大小

说明

Elf32_Addr

4

无符号程序地址

Elf32_Half

2

无符号中等整数

Elf32_Off

4

无符号文件偏移

Elf32_SWord

4

有符号大整数

Elf32_Word

4

无符号大整数

unsigned char

1

无符号笑整数

Elf32_Ehdr 各个成员简介:

  • e_ident magic bytes (0x7fELF), class, ABI version....是一组包含多个标志的数组。
  • e_typeobject file type—ET{REL,DYN,EXEC,CORE}
    00-未知, 01--RT_REL,02--ET_EXEC, 03---ET_DY
  • e_machine required architecture—EM X86 64, ... 其中3h=386, 28h=ARM
  • e_version EV CURRENT, always ”1”
  • e_entry virt. addr. of entry point, dl start, jmp *%r12
  • e_phoff program header offset
  • e_shoff section header offset
  • e_flags CPU-specific flags
  • e_ehsize ELF header size
  • e_phentsize size of program header entry, consistency check
  • e_phnum number of program header entries
  • e_shentsize size of section header entry
  • e_shnum number of section header entries
  • e_shstrndx section header string table index

其中e_ident 对应了对各字段,有MAGIC、Class、Data、Version、 ABI这几个参数。

Name

Value

Purpose

EI_MAG0

0

File identification

EI_MAG1

1

File identification

EI_MAG2

2

File identification

EI_MAG3

3

File identification

EI_CLASS

4

File class

EI_DATA

5

Data encoding

EI_VERSION

6

File version

EI_OSABI

7

Operating system/ABI identification

EI_ABIVERSION

8

ABI version

EI_PAD

9

Start of padding bytes

EI_NIDENT

16

Size of
e_ident[]

1)MAGIC是ELF标志码:魔数,占4字节,byte0固定为0x7F,byte1--byte3是'E', 'L', 'F' 的ASCII码。

2)EI_CLASS 是CPU字长类型。

Name

Value

Meaning

ELFCLASSNONE

0

Invalid class

ELFCLASS32

1

32-bit objects

ELFCLASS64

2

64-bit objects

3)EI_DATA

0:非法格式

1:小端字节序LSB

2:大端字节序MSB

4)EI_VERSION: ELF版本号,为1

5)EI_OSABI

Name

Value

Meaning

ELFOSABI_NONE

0

No extensions or unspecified

ELFOSABI_HPUX

1

Hewlett-Packard HP-UX

ELFOSABI_NETBSD

2

NetBSD

ELFOSABI_LINUX

3

Linux

ELFOSABI_SOLARIS

6

Sun Solaris

ELFOSABI_AIX

7

AIX

ELFOSABI_IRIX

8

IRIX

ELFOSABI_FREEBSD

9

FreeBSD

ELFOSABI_TRU64

10

Compaq TRU64 UNIX

ELFOSABI_MODESTO

11

Novell Modesto

ELFOSABI_OPENBSD

12

Open BSD

ELFOSABI_OPENVMS

13

Open VMS

ELFOSABI_NSK

14

Hewlett-Packard Non-Stop Kernel

64-255

Architecture-specific value range

3、段表--Section

1)段表的结构

ELF 文件中有很多段,段表(Section Header Table)就是保存这些段的基本属性的结构。段表描述了ELF各个段的信息,如段名、段的长度、在文件中的偏移、读写权限等。段表在ELF文件中 的偏移是由文件头的e_shoff成员决定的。

typedef struct{
Elf32_Word sh_name;
Elf32_Word sh_type;
Elf32_Word sh_flags;
Elf32_Addr sh_addr;
Elf32_Off sh_offset;
Elf32_Word sh_size;
Elf32_Word sh_link;
Elf32_Word sh_info;
Elf32_Word sh_addralign;
Elf32_Word sh_entsize;
}Elf32_Shdr

Elf32_Shdr成员含义:

  • sh_name 段名,但此次只是记录了段名字符串在 .shstrtab 中的偏移
  • sh_type 段的类型
  • sh_flags 段的标志位
  • sh_addr 段的虚拟地址,如果此段可以被加载则表示在进程中的虚拟地址,否则为0
  • sh_offset 如果此段位于文件中则表示此段在文件中的偏移
  • sh_size 段的长度
  • sh_link This member holds a section header table index link, whose interpretation depends on the section type.
  • sh_info This member holds extra information, whose interpretation depends on the section type.
  • sh_addralign 段对齐,以2的n次方表示,如果为0或1,表示没有对齐要求。
  • sh_entsize Section Entry Size段的长度

sh_type :段的类型。

Name

Value

SHT_NULL

0

SHT_PROGBITS

1

SHT_SYMTAB

2

SHT_STRTAB

3

SHT_RELA

4

SHT_HASH

5

SHT_DYNAMIC

6

SHT_NOTE

7

SHT_NOBITS

8

SHT_REL

9

SHT_SHLIB

10

SHT_DYNSYM

11

SHT_LOPROC

0x70000000

SHT_HIPROC

0x7fffffff

SHT_LOUSER

0x80000000

SHT_HIUSER

0xffffffff

sh_flag:段的标志位,表示该段在进程的虚拟地址空间中的属性,如是否可读、写、执行。

Name

Value

notes

SHF_WRITE

0x1

可读

SHF_ALLOC

0x2

需要分配空间

SHF_EXECINSTR

0x4

可执行

SHF_MASKPROC

0xf0000000

sh_link 和 sh_info: 如果段是与链接相关的,比如重定位表符号表等,sh_link和sh_info这两个成员所包含的意义如下所示。

sh_type

sh_link

sh_info

SHT_DYNAMIC

该段所使用的字符串表在段表中的下标

0

SHT_HASH

该段所使用的符号表在段表中的下标

0

SHT_REL

该段所使用的相应符号表在段表中的下标

该重定位表所作用的段在段表中的下标

SHT_RELA

该段所使用的相应符号表在段表中的下标

该重定位表所作用的段在段表中的下标

SHT_SYMTAB

操作系统相关的

操作系统相关的

SHT_DYNAMIC

操作系统相关的

操作系统相关的

other

SHN_UNDEF

0

2)重定位表

rel.txt段就是重定位表,类型sh_type为SHT_REL(9)。链接器在处理目标文件时,对目标文件某些部位进行重定位,即代码段和数据段中的那部分绝对地址引用。这些重定位信息记录在ELF文件的重定位表中。

3)字符串表

ELF中有很多字符串,如变量名段名等,但是字符串长度是不定长的,如果用固定的长度来表示比较困难,常见的做法是把字符串集中起来放到一个表中,然后使用字符串在表中的偏移来引用字符串。

4)符号表

在ELF文件中,把函数和变量统称为符号(Sysbol),每个符号有一个相应的值叫做符号值。对函数和变量来说,符号值就是它们的地址。在ELF文件中,用.symtab这个段来记录符号表。

typedef struct{
Elf32_Word st_name;
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned
charst_info;
unsigned
charst_other;
Elf32_Half st_shndx;
}Elf32_Sym
  • st_name
    符号名,符号名称在字符串表中的索引
  • st_value
    符号相应的值,可能是地址或一个绝对值数
  • st_size
    符号大小
  • st_info
    符号类型和绑定值
  • st_other
    默认0
  • st_shndx
    符号所在的段

st_info 高4位表示符号绑定信息,低4位表示符号类型。

Symbol Binding, ELF32_ST_BIND

Name

Value

STB_LOCAL

0

STB_GLOBAL

1

STB_WEAK

2

STB_LOPROC

13

STB_HIPROC

15

Symbol Types, ELF32_ST_TYPE

Name

Value

STT_NOTYPE

0

STT_OBJECT

1

STT_FUNC

2

STT_SECTION

3

STT_FILE

4

STT_LOPROC

13

STT_HIPROC

15

5)全局偏移表和跳转表

.plt和.got 动态链接的跳转表和全局入口表。

6)其它

代码段(.text): 代码数据

数据段(.data): 初始化过了的全局变量和局部静态变量

只读数据段(.rodata) 只读数据如const值、字符串常量。

.bss段:未初始化的全局变量和局部变量,是否为全局变量和局部变量预留空间和编译器的实现相关。

自定义段:在变量和函数前加__attribute__((section("name"))) 属性就可以把相应的函数或变量放到以name作为段名的段中。

4、ELF文件加载视图

虽然ELF把可变数据和不可变数据分的很细,用户也可以自己添加段或命名一个段,但加载器把ELF文件加载到内存时,并不按照ELF的结构读取。例如目标文件.o里的代码段.text是section,链接时多个可重定位文件整合成一个可执行的文件,为了提高程序的效率,链接器把目标文件中相同的section 整合成一个segment,方便运行时加载器加载程序。由于虚拟内存的映射和优化的存在,ELF文件在加载到虚拟内存时,也会合并不同的段来达到节约资源的目的。

5、参考文献

1、Linux Referenced Specifications
https://refspecs.linuxbase.org/

2、Executable and Linkable Format (ELF)
https://elinux.org/Executable_and_Linkable_Format_(ELF)

3、ELF文件格式
https://zhuanlan.zhihu.com/p/286088470

尊重原创技术文章,转载请注明:
https://www.cnblogs.com/pingwen/p/17323862.html