IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 系统运维 -> Dubbo负载均衡策略初学(一) -> 正文阅读

[系统运维]Dubbo负载均衡策略初学(一)

负载均衡LoadBalance

含义:表示将一些网络请求或者其他形式的负载分摊到机器上。通过负载均衡,可以让每一台服务器负责处理适合自己能力的负载,达到让高负载服务器分流,以及避免资源浪费的目的。

LoadBalance是一个接口,只提供一个对外暴露的方法。

@SPI("random")
public interface LoadBalance {
    @Adaptive({"loadbalance"})
    <T> Invoker<T> select(List<Invoker<T>> var1, URL var2, Invocation var3) throws RpcException;
}

@SPI:标注是一个扩展点,各种实现配置在配置文件里(key是实现名字,类似spring里的bean name,value是实现类)

@Adaptive:Adaptive 可注解在类或方法上。当 Adaptive 注解在类上时,Dubbo 不会为该类生成代理类。

AbstractLoadBalance:是dubbo所有负载均衡策略的父类,dubbo的负载均衡策略均实现该类。

public abstract class AbstractLoadBalance implements LoadBalance {
    public AbstractLoadBalance() {
    }

    // 用来计算权重,随着uptime增大,ww会逐渐接近weight
    static int calculateWarmupWeight(int uptime, int warmup, int weight) {
        int ww = (int)((float)uptime / ((float)warmup / (float)weight));
        return ww < 1 ? 1 : (ww > weight ? weight : ww);
    }

    public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        if (invokers != null && !invokers.isEmpty()) {
            boolean mustProviderIpAndPort = Boolean.valueOf(invocation.getAttachment("_must_provider_ip_port_", "false"));
            if (invokers.size() == 1 && !mustProviderIpAndPort) {
                return (Invoker)invokers.get(0);
            } else {
                String providerIp = invocation.getAttachment("_provider_ip_", "");
                String providerPort = invocation.getAttachment("_provider_port_", "");
                if (!providerIp.equals("") && !providerPort.equals("")) {
                    Optional<Invoker<T>> optional = invokers.stream().filter((it) -> {
                        return it.getUrl().getHost().equals(providerIp) && it.getUrl().getPort() == Integer.valueOf(providerPort);
                    }).findFirst();
                    if (optional.isPresent()) {
                        return this.doSelect(Arrays.asList((Invoker)optional.get()), url, invocation);
                    }

                    if (mustProviderIpAndPort) {
                        throw new RpcException("No provider available in " + invokers + " ip:" + providerIp + " ,port:" + providerPort);
                    }
                }

                String consumerRegion = url.getParameter("consumer.region", "");
                if (StringUtils.isNotEmpty(consumerRegion)) {
                    List<Invoker<T>> invokerList = (List)invokers.stream().filter((it) -> {
                        return it.getUrl().getParameter("provider.region", "").equals(consumerRegion);
                    }).collect(Collectors.toList());
                    if (invokerList.size() > 0) {
                        return this.doSelect(invokerList, url, invocation);
                    }
                }

                return this.doSelect(invokers, url, invocation);
            }
        } else {
            return null;
        }
    }

    // 调用负载均衡,由子类实现
    protected abstract <T> Invoker<T> doSelect(List<Invoker<T>> var1, URL var2, Invocation var3);

    // 权重的计算过程,该过程主要用于保证当服务运行时长小于服务预热时间时,对服务进行降权,
    // 避免让服务在启动之初就处于高负载状态。
    protected int getWeight(Invoker<?> invoker, Invocation invocation) {
        int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), "weight", 100);
        if (weight > 0) {
            long timestamp = invoker.getUrl().getParameter("remote.timestamp", 0L);
            if (timestamp > 0L) {
                int uptime = (int)(System.currentTimeMillis() - timestamp);
                int warmup = invoker.getUrl().getParameter("warmup", 600000);
                if (uptime > 0 && uptime < warmup) {
                    weight = calculateWarmupWeight(uptime, warmup, weight);
                }
            }
        }

        return weight;
    }
}

Invoker:调用者,对应一个服务接口。Invoker是Dubbo的核心模型,其它模型都向它靠拢,或转换成它,通过invoke方法执行调用,参数为Invocation(一次具体的调用,持有调用过程中的变量,包含方法名、参数类型、参数、调用者等),返回值为Result。

dubbo负载均衡策略包括Random LoadBalanceRoundRobin LoadBalanceLeastAction LoadBalanceConsistentHash LoadBalance

Random LoadBalance:基于随机加权算法,是dubbo的默认负载均衡策略。

随机加权算法:一般应用在以下场景——有一个集合S,里面比如有A,B,C这三项,这时我们想随机从中生成一项,但是生成的概率不同,比如我们希望生成A的概率是50%,B的概率是30%,C的概率是20%。一般来说,我们可以给各项附一个权重,生成的概率正比于这个权重。如图。

?

只要随机数生成器产生的随机数分布性很好,在经过多次选择后,每个服务器被选中的次数比例接近其权重比例。

源码:

public class RandomLoadBalance extends AbstractLoadBalance {
    public static final String NAME = "random";

    public RandomLoadBalance() {
    }

    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        int length = invokers.size();
        boolean sameWeight = true;
        int firstWeight = this.getWeight((Invoker)invokers.get(0), invocation);
        int totalWeight = firstWeight;

        int offset;
        int i;
        // 计算总权重totalWeight
        // 比较当前服务的权重与之前服务的的权重是否相同
        for(offset = 1; offset < length; ++offset) {
            i = this.getWeight((Invoker)invokers.get(offset), invocation);
            totalWeight += i;
            if (sameWeight && i != firstWeight) {
                sameWeight = false;
            }
        }

        // 计算随机数并判断落在那个服务上
        if (totalWeight > 0 && !sameWeight) {
            offset = ThreadLocalRandom.current().nextInt(totalWeight);
            // 如果是三个服务A、B、C,该三个服务的权重分别为(5,3,2),
            // 如果随机数是7,那么7-5=2>0表明该随机数不在A的[0,5)范围内,
            // 然后继续循环,2-3=-1<0表明该随机数在B的[5,8)范围,结束循环
            for(i = 0; i < length; ++i) {
                offset -= this.getWeight((Invoker)invokers.get(i), invocation);
                if (offset < 0) {
                    return (Invoker)invokers.get(i);
                }
            }
        }

        // 如果服务的权重都相同,那么随机返回一个数就可以
        return (Invoker)invokers.get(ThreadLocalRandom.current().nextInt(length));
    }
}

RoundRobin LoadBalance:基于加权轮询算法

轮询算法:将请求轮流分配给服务器。轮询算法假设所有服务器的处理性能都相同,不关心每台服务器的当前连接数和响应速度。这种情况现实中很难实现,所以我们采用加权轮询算法。

加权轮询算法的原理:根据服务器的不同处理能力,给每个服务器分配不同的权值,使其能够接受相应权值数的服务请求。

经过加权后,每台服务器能够得到的请求数比例,接近或等于他们的权重比。

加权轮询算法的结果,就是要生成一个服务器序列。每当有请求到来时,就依次从该序列中取出下一个服务器用于处理该请求。

比如有三个服务A、B、C,它们的权重分别为5,3,2。那么经过加权轮询算法,会生成{A,B,A,B,A,B,A,C,A,C}这样的序列,然后根据收到的请求,会把5个请求分配给A,3个请求分配给B,2个请求分配给C。

源码:

public class RoundRobinLoadBalance extends AbstractLoadBalance {
    public static final String NAME = "roundrobin";
    private static int RECYCLE_PERIOD = 60000;
    private ConcurrentMap<String, ConcurrentMap<String, RoundRobinLoadBalance.WeightedRoundRobin>> methodWeightMap = new ConcurrentHashMap();
    private AtomicBoolean updateLock = new AtomicBoolean();

    public RoundRobinLoadBalance() {
    }

    protected <T> Collection<String> getInvokerAddrList(List<Invoker<T>> invokers, Invocation invocation) {
        // 获得服务的全限定名+"."+方法名,比如com.test.TestService.selectOne
        // 全限定名就是类名全称,带包路径的用点隔开,比如,java.lang.String
        // 非全限定名就是短名,也就是String
        String key = ((Invoker)invokers.get(0)).getUrl().getServiceKey() + "." + invocation.getMethodName();
        
        Map<String, RoundRobinLoadBalance.WeightedRoundRobin> map = (Map)this.methodWeightMap.get(key);
        return map != null ? map.keySet() : null;
    }

    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        String key = ((Invoker)invokers.get(0)).getUrl().getServiceKey() + "." + invocation.getMethodName();
        // 获取 url 到 WeightedRoundRobin 映射表,如果为空,则创建一个新的
        ConcurrentMap<String, RoundRobinLoadBalance.WeightedRoundRobin> map = (ConcurrentMap)this.methodWeightMap.get(key);
        if (map == null) {
            this.methodWeightMap.putIfAbsent(key, new ConcurrentHashMap());
            map = (ConcurrentMap)this.methodWeightMap.get(key);
        }
        // 总权重
        int totalWeight = 0;
        // 最大权重
        long maxCurrent = -9223372036854775808L;
        // 获取当前系统时间
        long now = System.currentTimeMillis();
        Invoker<T> selectedInvoker = null;
        RoundRobinLoadBalance.WeightedRoundRobin selectedWRR = null;

        int weight;
        // invokers列表遍历
        for(Iterator var13 = invokers.iterator(); var13.hasNext(); totalWeight += weight) {
            Invoker<T> invoker = (Invoker)var13.next();
            // 存储 url 唯一标识 identifyString 到 weightedRoundRobin 的映射关系
            String identifyString = invoker.getUrl().toIdentityString();
            RoundRobinLoadBalance.WeightedRoundRobin weightedRoundRobin = (RoundRobinLoadBalance.WeightedRoundRobin)map.get(identifyString);
            // 获取当前权重
            weight = this.getWeight(invoker, invocation);
            if (weight < 0) {
                weight = 0;
            }

            // 检测当前Invoker是否有对应的weightedRoundRobin,没有则创建
            if (weightedRoundRobin == null) {
                weightedRoundRobin = new RoundRobinLoadBalance.WeightedRoundRobin();
                // 设置权重
                weightedRoundRobin.setWeight(weight);
                map.putIfAbsent(identifyString, weightedRoundRobin);
            }

            // weightedRoundRobin的权重不等于当前权重,表示权重变化,更新weightedRoundRobin
            if (weight != weightedRoundRobin.getWeight()) {
                weightedRoundRobin.setWeight(weight);
            }
            
            // 即current+=weight
            long cur = weightedRoundRobin.increaseCurrent();
            // 将lastUpdate设置为当前系统时间,表示更新了。
            weightedRoundRobin.setLastUpdate(now);
            // 取最大值
            if (cur > maxCurrent) {
                maxCurrent = cur;
                // 当前cur为最大值时,选择的Invoker设置为当前invoker
                selectedInvoker = invoker;
                // 保留weightedRoundRobin以做后用
                selectedWRR = weightedRoundRobin;
            }
        }

        // 过滤长时间未被更新的节点
        if (!this.updateLock.get() && invokers.size() != map.size() && this.updateLock.compareAndSet(false, true)) {
            try {
                ConcurrentMap<String, RoundRobinLoadBalance.WeightedRoundRobin> newMap = new ConcurrentHashMap();
                newMap.putAll(map);
                // 遍历,移除长时间未更新的节点
                Iterator it = newMap.entrySet().iterator();
                while(it.hasNext()) {
                    Entry<String, RoundRobinLoadBalance.WeightedRoundRobin> item = (Entry)it.next();
                    if (now - ((RoundRobinLoadBalance.WeightedRoundRobin)item.getValue()).getLastUpdate() > (long)RECYCLE_PERIOD) {
                        it.remove();
                    }
                }
                
                // 更新引用
                this.methodWeightMap.put(key, newMap);
            } finally {
                this.updateLock.set(false);
            }
        }

        if (selectedInvoker != null) {
            selectedWRR.sel(totalWeight);
            return selectedInvoker;
        } else {
            return (Invoker)invokers.get(0);
        }
    }

    protected static class WeightedRoundRobin {
        private int weight;
        private AtomicLong current = new AtomicLong(0L);
        private long lastUpdate;

        protected WeightedRoundRobin() {
        }

        public int getWeight() {
            return this.weight;
        }

        public void setWeight(int weight) {
            this.weight = weight;
            this.current.set(0L);
        }

        public long increaseCurrent() {
            return this.current.addAndGet((long)this.weight);
        }

        public void sel(int total) {
            this.current.addAndGet((long)(-1 * total));
        }

        public long getLastUpdate() {
            return this.lastUpdate;
        }

        public void setLastUpdate(long lastUpdate) {
            this.lastUpdate = lastUpdate;
        }
    }
}

LeastActive?LoadBalance:基于最小活跃数负载均衡算法

最小活跃数负载均衡算法:假设初始情况下,每个服务提供者活跃数为0,接受到一个请求,活跃数加一,完成请求后,活跃数减一。在相同时间内,性能好的服务提供者能够更快速的完成请求,活跃数减少得快,因此能够分配到更多的请求。

而LeastActiveLoadBalance在此基础上引入了权重值。比如,一个请求来临时,两个服务提供者的权重值更高的那个可以接受请求,两者权重值相等时,随机选择一者。

源码:

public class LeastActiveLoadBalance extends AbstractLoadBalance {
    public static final String NAME = "leastactive";

    public LeastActiveLoadBalance() {
    }

    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        int length = invokers.size();
        // 最小活跃数
        int leastActive = -1;
        // 记录最小活跃数相同的服务提供者Invoker的数量
        int leastCount = 0;
        // 记录具有相同最小活跃数的Invoker在invokers列表的下标位置
        int[] leastIndexes = new int[length];
        int totalWeight = 0;
        // 第一个具有最小活跃数的Invoker的权重值
        int firstWeight = 0;
        boolean sameWeight = true;

        // 遍历invokers列表
        int offsetWeight;
        int leastIndex;
        for(offsetWeight = 0; offsetWeight < length; ++offsetWeight) {
            //获取对应权重的Invoker
            Invoker<T> invoker = (Invoker)invokers.get(offsetWeight);
            // 获取Invoker对应的活跃数
            leastIndex = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive();
            // 获取权重
            int afterWarmup = this.getWeight(invoker, invocation);
            // 未发现最小活跃数
            if (leastActive != -1 && leastIndex >= leastActive) {
                // 当前活跃数与最小活跃数相同
                if (leastIndex == leastActive) {
                    // 具有最小活跃数的Invoker数量累加,记录invokers列表下标位置
                    leastIndexes[leastCount++] = offsetWeight;
                    // 累加总权重
                    totalWeight += afterWarmup;
                    // 如果当前权重不等于firstWeight,则将sameWeight设置为false
                    if (sameWeight && offsetWeight > 0 && afterWarmup != firstWeight) {
                        sameWeight = false;
                    }
                }
            } else {// 发现更小活跃数,重新开始
                leastActive = leastIndex;
                leastCount = 1;
                leastIndexes[0] = offsetWeight;
                totalWeight = afterWarmup;
                firstWeight = afterWarmup;
                sameWeight = true;
            }
        }

        // 具有最小活跃数的Invoker只有一个时,直接返回即可
        if (leastCount == 1) {
            return (Invoker)invokers.get(leastIndexes[0]);
        } else {
            // 当具有最小活跃数的Invoker有多个且权重不相等时
            if (!sameWeight && totalWeight > 0) {
                // 获得一个[1,totalWeight+1)范围的随机数
                offsetWeight = ThreadLocalRandom.current().nextInt(totalWeight) + 1;

                for(int i = 0; i < leastCount; ++i) {
                    leastIndex = leastIndexes[i];
                    // 获取权重值,让随机数减去权重值
                    offsetWeight -= this.getWeight((Invoker)invokers.get(leastIndex), invocation);
                    if (offsetWeight <= 0) {
                        return (Invoker)invokers.get(leastIndex);
                    }
                }
            }
            
            // 当有多个具有最小活跃值的Invoker且权重值都相等时,随机返回一个即可
            return (Invoker)invokers.get(leastIndexes[ThreadLocalRandom.current().nextInt(leastCount)]);
        }
    }
}

ConsistentHashLoadBalance:基于哈希一致性算法

哈希算法:根据结点的关键码值来确定其存储地址。

举例:有三台服务器A、B、C,有十个请求需要分配给三台服务器处理,可以通过hash(请求地址)%3获得一个数,这个数一定在0,1,2三个数据中,然后0对应A服务器,1对应B服务器,2对应C服务器就可以处理这些请求了。

但是当服务器增多后,不再是模3了,计算结果会改变,从而导致之前计算的数据都会失效,这种情况下,就可以采用一致性哈希算法。

一致性哈希算法:与哈希算法类似,不过公式变为hash(请求地址)%2^32,并且需要两者都要通过公式进行计算映射。

可以将2^32想象为一个圆,假设这个圆是由2^32个点组成。

?我们把这个由2的32次方个点组成的圆环称为hash环。

在一致性哈希算法中,需要对服务器进行计算,还是之前的三个服务器A、B、C,对于每个服务器都hash(服务器Ip地址)%2^32,然后将服务器对应到hash环上。

?有二十个请求,需要分配到三个服务器上,同样也用公式计算,hash(请求地址)%2^32,然后将请求映射到hash环上,从请求映射的位置出发,顺时针旋转,遇到的第一个服务器,就是给请求分配的服务器。

ConsistentHashLoadBalance就是根据这种思想实现的。同时在这里引入了虚拟节点。

虚拟节点:让 Invoker 在圆环上分散开来,避免数据倾斜问题。

数据倾斜问题:由于节点不够分散,导致大量请求落到了同一个节点上,而其他节点只会接收到了少量请求的情况。

源码:

public class ConsistentHashLoadBalance extends AbstractLoadBalance {
    public static final String NAME = "consistenthash";
    private final ConcurrentMap<String, ConsistentHashLoadBalance.ConsistentHashSelector<?>> selectors = new ConcurrentHashMap();

    public ConsistentHashLoadBalance() {
    }

    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        String methodName = RpcUtils.getMethodName(invocation);
        // 获得服务的全限定名+"."+方法名
        String key = ((Invoker)invokers.get(0)).getUrl().getServiceKey() + "." + methodName;
        // 获取invokers列表原始的Hashcode
        int identityHashCode = System.identityHashCode(invokers);
        ConsistentHashLoadBalance.ConsistentHashSelector<T> selector = (ConsistentHashLoadBalance.ConsistentHashSelector)this.selectors.get(key);
        // 服务提供者的数量发生了变化时
        if (selector == null || selector.identityHashCode != identityHashCode) {
            // 创建新的 ConsistentHashSelector
            this.selectors.put(key, new ConsistentHashLoadBalance.ConsistentHashSelector(invokers, methodName, identityHashCode));
            selector = (ConsistentHashLoadBalance.ConsistentHashSelector)this.selectors.get(key);
        }

        // 返回选择的Invoker
        return selector.select(invocation);
    }

    private static final class ConsistentHashSelector<T> {
        // 用TreeMap存储Invoker的虚拟节点
        private final TreeMap<Long, Invoker<T>> virtualInvokers = new TreeMap();
        private final int replicaNumber;
        private final int identityHashCode;
        private final int[] argumentIndex;
        
        ConsistentHashSelector(List<Invoker<T>> invokers, String methodName, int identityHashCode) {
            this.identityHashCode = identityHashCode;
            URL url = ((Invoker)invokers.get(0)).getUrl();
            // 获取虚拟节点数,默认值为160
            this.replicaNumber = url.getMethodParameter(methodName, "hash.nodes", 160);
            // 获取参与hash计算的参数下标值,默认对第一个参数进行hash计算
            String[] index = Constants.COMMA_SPLIT_PATTERN.split(url.getMethodParameter(methodName, "hash.arguments", "0"));
            this.argumentIndex = new int[index.length];

            for(int i = 0; i < index.length; ++i) {
                this.argumentIndex[i] = Integer.parseInt(index[i]);
            }

            // 对invokers列表进行遍历
            Iterator var14 = invokers.iterator();
            while(var14.hasNext()) {
                Invoker<T> invoker = (Invoker)var14.next();
                String address = invoker.getUrl().getAddress();
                for(int i = 0; i < this.replicaNumber / 4; ++i) {
                    // 对address+i进行mds5计算,获得一个长度为16的字节数组
                    byte[] digest = this.md5(address + i);
                    
                    // 对于digest进行四次hash计算
                    for(int h = 0; h < 4; ++h) {
                        long m = this.hash(digest, h);
                        this.virtualInvokers.put(m, invoker);
                    }
                }
            }

        }

        public Invoker<T> select(Invocation invocation) {
            // 将参数转为 key
            String key = this.toKey(invocation.getArguments());
            // 对key进行md5计算
            byte[] digest = this.md5(key);
            return this.selectForKey(this.hash(digest, 0));
        }

        private String toKey(Object[] args) {
            StringBuilder buf = new StringBuilder();
            int[] var3 = this.argumentIndex;
            int var4 = var3.length;

            for(int var5 = 0; var5 < var4; ++var5) {
                int i = var3[var5];
                if (i >= 0 && i < args.length) {
                    buf.append(args[i]);
                }
            }

            return buf.toString();
        }

        private Invoker<T> selectForKey(long hash) {
            Entry<Long, Invoker<T>> entry = this.virtualInvokers.ceilingEntry(hash);
            // 如果 hash 大于 Invoker 在圆环上最大的位置,此时 entry = null,
            // 需要将 TreeMap 的头节点赋值给 entry
            if (entry == null) {
                entry = this.virtualInvokers.firstEntry();
            }

            return (Invoker)entry.getValue();
        }

        private long hash(byte[] digest, int number) {
            return ((long)(digest[3 + number * 4] & 255) << 24 | (long)(digest[2 + number * 4] & 255) << 16 | (long)(digest[1 + number * 4] & 255) << 8 | (long)(digest[number * 4] & 255)) & 4294967295L;
        }

        private byte[] md5(String value) {
            MessageDigest md5;
            try {
                md5 = MessageDigest.getInstance("MD5");
            } catch (NoSuchAlgorithmException var6) {
                throw new IllegalStateException(var6.getMessage(), var6);
            }

            md5.reset();

            byte[] bytes;
            try {
                bytes = value.getBytes("UTF-8");
            } catch (UnsupportedEncodingException var5) {
                throw new IllegalStateException(var5.getMessage(), var5);
            }

            md5.update(bytes);
            return md5.digest();
        }
    }
}

  系统运维 最新文章
配置小型公司网络WLAN基本业务(AC通过三层
如何在交付运维过程中建立风险底线意识,提
快速传输大文件,怎么通过网络传大文件给对
从游戏服务端角度分析移动同步(状态同步)
MySQL使用MyCat实现分库分表
如何用DWDM射频光纤技术实现200公里外的站点
国内顺畅下载k8s.gcr.io的镜像
自动化测试appium
ctfshow ssrf
Linux操作系统学习之实用指令(Centos7/8均
上一篇文章      下一篇文章      查看所有文章
加:2022-03-30 19:10:50  更:2022-03-30 19:13:10 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/16 0:00:38-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码