Giter VIP home page Giter VIP logo

blog's Introduction

个人博客

Flag Counter

主要用于记录一些平常的学习总结,可能会涉及到方方面面、中间件、并发、源码解析等等,感兴趣的请点击star或者watch,不要点fork哦!

目录:

blog's People

Contributors

acoder2013 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

blog's Issues

下一代分布式消息队列Apache Pulsar从入门到实现(一)

Pulsar简介

Apache Pulsar是一个企业级的分布式消息系统,最初由Yahoo开发并在2016年开源,目前正在Apache基金会下孵化。Plusar已经在Yahoo的生产环境使用了三年多,主要服务于Mail、Finance、Sports、 Flickr、 the Gemini Ads platform、 Sherpa以及Yahoo的KV存储。
Pulsar之所以能够称为下一代消息队列,主要是因为以下特性:

  1. 线性扩展。能够丝滑的扩容到成百上千个节点(Kafka扩容需要占用很多系统资源在节点间拷贝数据,而Plusar完全不用)
  2. 高吞吐。已经在Yahoo的生产环境中经受了考验,每秒数百万消息
  3. 低延迟。在大规模的消息量下依然能够保持低延迟(< 5ms)
  4. 持久化机制。Plusar的持久化机制构建在Apache BookKeeper之上,提供了写与读之前的IO隔离
  5. 基于地理位置的复制。Plusar将多地域/可用区的复制作为首要特性支持。用户只需配置好可用区,消息就会被源源不断的复制到其他可用区。当某一个可用区挂掉或者发生网络分区,plusar会在之后不断的重试。
  6. 部署方式的多样化。既可以运行在裸机,也支持目前例如Docker、K8S的一些容器化方案以及不同的云厂商,同时在本地开发时也只需要一行命令即可启动整个环境。
  7. Topic支持多种消费模式:exclusive、shared、failover

架构概述

从最上层来看,一个Plusar单元由若干个集群组成,单元内的集群可以互相之前复制数据, plusar中通常有以下几种组件:

  1. Broker:负责处理Producer发来的消息并分发给消费者。通过一个全局的ZK集群来处理多种协作式任务,例如说基于地理位置的复制。并将消息存储到BookKeeper中,同时单个集群内也需要有一套ZK集群,来存储一些元数据。
  2. BookKeeper集群: 内部包含多个bookies,用于持久化消息。
  3. ZooKeeper集群
    image

Broker

在Kafka和RocketMQ中,Broker负责消息数据的存储以及consumer消费位移的存储等,而Plusar中的broker和他们两个有所不同,plusar中的broker是一个无状态的节点,主要负责三件事情:

  1. 暴露REST接口用于执行管理员的命令以及topic所有者的查询等
  2. 一个用于节点间通讯的异步的TCP服务器,协议目前采用的是Google之前开源的Protocol Buffer
  3. 为了支持地域复制,broker会将自己 集群所在的消息发布到其他可用区。

消息会被先发布到BookKeeper中,然后会在Broker本地内存中缓存一份,因此一般来说消息的读取都会从从内存中读取,因此第一条中所说的查找topic所有者就是说,因为BookKeeper中的一个ledger只允许一个writer,因此我们可以调用rest接口获取到某一个topic当前的所有者。

BookKeeper

BookKeeper是一个可横向扩展的、错误容忍的、低延迟的分布式存储服务,BookKeeper中最基本的单位是记录,实际上就一个字节数组,而记录的数组称之为ledger,BK会将记录复制到多个bookies,存储ledger的节点叫做bookies,从而获得更高的可用性和错误容忍性。从设计阶段BK就考虑到了各种故障,Bookies可以宕机、丢数据、脏数据,但是主要整个集群中有足够的Bookies服务的行为就是正确的。
在Pulsar中,每个分区topic是由若干个ledger组成的,而ledger是一个append-only的数据结构,只允许单个writer,ledger中的每条记录会被复制到多个bookies中,一个ledger被关闭后(例如broker宕机了或者达到了一定的大小)就只支持读取,而当ledger中的数据不再需要的时候(例如所有的消费者都已经消费了这个ledger中的消息)就会被删除。
image

Bookkeeper的主要优势在于它可以保证在出现故障时在ledger的读取一致性。因为ledger只能被同时被一个writer写入,因为没有竞争,BK可以更高效的实现写入。在Broker宕机后重启时,Plusar会启动一个恢复的操作,从ZK中读取最后一个写入的Ledger并读取最后一个已提交的记录,然后所有的消费者也都被保证能看到同样的内容。
image

我们知道Kafka在0.8版本之前是将消费进度存储到ZK中的,但是ZK本质上基于单个日志的中心服务,简单来讲,ZK的性能不会随着你增加更多的节点而线性增加,会只会相反减少,因为更多的节点意味着需要将日志同步到更多的节点,性能也会随之下降,因此QPS也会受单机性能影响,因此0.8版本之后就将消费进度存储到了Kafka的Topic中,而RocketMQ最初的版本也类似,有几种不同的实现例如ZK、数据库等,目前版本采用的是存储到本机文件系统中,而Plusar采用了和Kafka类似的**,Plusar将消费进度也存储到了BK的ledger中。

image

元数据

Plusar中的元数据主要存储到ZK中,例如不同可用区相关的配置会存在全局的ZK中,集群内部的ZK用于存储例如某个topic的数据写入到了那些Ledger、Broker目前的一些埋点数据等等

Plusar核心概念

Topic

发布订阅系统中最核心的概念是topic,简单来说,topic可以理解为一个管道,producer可以往这个管道丢消息,consumer可以从这个管道的另一端读取消息,但是这里可以有多个consumer同时从这个管道读取消息。
image
每个topic可以划分为多个分区,同一个topic下的不同分区所包含的消息都是不同的。每个消息在被添加到一个分区后都会分配一个唯一的offset,在同一个分区内消息是有序的,因此客户端可以根据比如说用户ID进行一个哈希取模从而使得整个用户的消息都发往整个分区,从而一定程度上避免race condition的问题。
通过分区,将大量的消息分散到不同的节点处理从而获得高吞吐。默认情况下,plusar的topic都是非分区的,但是支持通过cli或者接口创建一定分区数目的topic。
image
默认情况下Plusar会自动均衡Producer和Consumer,但有时候客户端想要根据自己的业务规则也进行路由,Plusar默认支持以下几种规则:单分区、轮询、哈希、自定义(即自己实现相关接口来定制路由规则)

消费模式

消费决定了消息具体是如何被分发到消费者的,Plusar支持几种不同的消费模式: exclusive、shared、failover。图示如下:
image

  1. Exclusive: 一个topic只能被一个消费者消费。Plusar默认就是这个模式
  2. Shared: 共享模式或者叫轮询模式,多个消费者可以连接到同一个topic,消息被依次分发给消费者,当一个消费者宕机或者主动断开连接,那么发到那个消费者的还没有ack的消息会得到重新调度分发给其他消费者。
  3. Failover: 多个消费者可以连接同一个topic并按照字典序排序,第一个消费者会开始消费消息,称之为master,当master断开连接,所有未ack和队列中剩下的消息会分发给另一个消费者。
    Plusar目前也支持另一种Reader接口,支持传入一个消息ID,例如说Message.Earliest来从最早的消息开始消费。

总结

Plusar作为下一代分布式消息队列,拥有非常多吸引人的特性,也弥补了一些其他竞品的短板,例如地域复制、多租户、扩展性、读写隔离等等。

Flag Counter

Atomic包之FieldUpdater深度解析

前言

Java 5 中由Doug Lea大神写的atomic classes 中引入了 Field Updater,本质上来说就是volatile 字段的包装器,下面我们看看该如何使用:

AtomicIntegerFieldUpdater

首先看看类的定义:

public abstract class AtomicIntegerFieldUpdater<T> {

    /**
    *  抽象方法,提供一个静态方法以便得到实例
    */
    @CallerSensitive
    public static <U> AtomicIntegerFieldUpdater<U> newUpdater(Class<U> tclass,
                                                              String fieldName) {
        return new AtomicIntegerFieldUpdaterImpl<U>
            (tclass, fieldName, Reflection.getCallerClass());
    }
}


private static final class AtomicIntegerFieldUpdaterImpl<T>
        extends AtomicIntegerFieldUpdater<T> {
        
    AtomicIntegerFieldUpdaterImpl(final Class<T> tclass,
                                      final String fieldName,
                                      final Class<?> caller) {
            final Field field;
            final int modifiers;
            try {
                field = AccessController.doPrivileged(
                    new PrivilegedExceptionAction<Field>() {
                        public Field run() throws NoSuchFieldException {
                            //字段不存在会抛异常
                            return tclass.getDeclaredField(fieldName);
                        }
                    });
                //检查访问级别,
                modifiers = field.getModifiers();
                sun.reflect.misc.ReflectUtil.ensureMemberAccess(
                    caller, tclass, null, modifiers);
                ClassLoader cl = tclass.getClassLoader();
                ClassLoader ccl = caller.getClassLoader();
                if ((ccl != null) && (ccl != cl) &&
                    ((cl == null) || !isAncestor(cl, ccl))) {
                    sun.reflect.misc.ReflectUtil.checkPackageAccess(tclass);
                }
            } catch (PrivilegedActionException pae) {
                throw new RuntimeException(pae.getException());
            } catch (Exception ex) {
                throw new RuntimeException(ex);
            }

            //必须是int
            if (field.getType() != int.class)
                throw new IllegalArgumentException("Must be integer type");
            //必须用volatile修饰
            if (!Modifier.isVolatile(modifiers))
                throw new IllegalArgumentException("Must be volatile type");

            this.cclass = (Modifier.isProtected(modifiers)) ? caller : tclass;
            this.tclass = tclass;
            //用Unsafe里的那一坨方法去原子更新
            this.offset = U.objectFieldOffset(field);
        }
}

那么有那些使用场景呢,我们可以看到Cassandra中有一些地方用到了,比如:

public abstract class AbstractWriteResponseHandler<T> implements IAsyncCallbackWithFailure<T>{
    private static final AtomicIntegerFieldUpdater<AbstractWriteResponseHandler> failuresUpdater
    = AtomicIntegerFieldUpdater.newUpdater(AbstractWriteResponseHandler.class, "failures");
    
    private volatile int failures = 0;

        public void get() throws WriteTimeoutException, WriteFailureException
    {
        long timeout = currentTimeout();

        boolean success;
        try
        {
            success = condition.await(timeout, TimeUnit.NANOSECONDS);
        }
        catch (InterruptedException ex)
        {
            throw new AssertionError(ex);
        }

        if (!success)
        {
            int blockedFor = totalBlockFor();
            int acks = ackCount();
            if (acks >= blockedFor)
                acks = blockedFor - 1;
            throw new WriteTimeoutException(writeType, consistencyLevel, acks, blockedFor);
        }

        //可以看到平常可以直接访问failures,而不用像AtomicInteger一样还需要调用get方法
        if (totalBlockFor() + failures > totalEndpoints())
        {
            throw new WriteFailureException(consistencyLevel, ackCount(), totalBlockFor(), writeType, failureReasonByEndpoint);
        }
    }

    @Override
    public void onFailure(InetAddress from, RequestFailureReason failureReason)
    {
        logger.trace("Got failure from {}", from);

        //当需要类似AtomicInteger一样的功能时,我们知道volatile执行++是非原子的
        int n = waitingFor(from)
                ? failuresUpdater.incrementAndGet(this)
                : failures;

        failureReasonByEndpoint.put(from, failureReason);

        if (totalBlockFor() + n > totalEndpoints())
            signal();
    }

}

小结

总体来看使用场景不多,有点类似AtomicInteger,但是可以使用比较方便,比如定义了一个变量x,你可以直接正常访问x,而如果使用了AtomicInteger的话,还是需要调用AtomicInteger#get方法,另外如果你又需要原子的get-set的话也可以满足使用场景;另外比如说你有个比较大的链表,内部每个节点都需要原子的get-set,那每个节点都要定义一个AtomicInteger,这样会消耗更多的内存,那么就可以定义个static的AtomicIntegerFieldUpdater。

AtomicReferenceFieldUpdater

和Integer差不多了,只不过这个是用来更新引用的,所以就不过多解读了,Java类库中BufferedInputStream就调用了这个类:

public
class BufferedInputStream extends FilterInputStream {

    protected volatile byte buf[];

    /*
    *  原子的更新内部数组,比如扩容、关闭时,
    */
    private static final
        AtomicReferenceFieldUpdater<BufferedInputStream, byte[]> bufUpdater =
        AtomicReferenceFieldUpdater.newUpdater
        (BufferedInputStream.class,  byte[].class, "buf");


    public void close() throws IOException {
        byte[] buffer;
        while ( (buffer = buf) != null) {
            //放在一个循环中,如果CAS更新失败,那么就读取最新的buf引用,继续CAS更新
            if (bufUpdater.compareAndSet(this, buffer, null)) {
                InputStream input = in;
                in = null;
                if (input != null)
                    input.close();
                return;
            }
        }
    }
}

另外还有JDK1.7之前的ConcurrentLinkedQueue,也有了这个机制,但是可以1.7之后切换到了Unsafe的方案, 主要还是因为性能原因,FieldUpdater的方案需要使用反射API配合,而Unsafe不用,而且有些JVM会把Unsafe的调用内联,理论上看会快很多(Todo :待测试)

//JDK1.6
private static class Node<E> {
        private volatile E item;
        private volatile Node<E> next;

        private static final
            AtomicReferenceFieldUpdater<Node, Node>
            nextUpdater =
            AtomicReferenceFieldUpdater.newUpdater
            (Node.class, Node.class, "next");
        private static final
            AtomicReferenceFieldUpdater<Node, Object>
            itemUpdater =
            AtomicReferenceFieldUpdater.newUpdater
            (Node.class, Object.class, "item");
}
//JDK1.8
private static class Node<E> {
        volatile E item;
        volatile Node<E> next;

        /**
         * Constructs a new node.  Uses relaxed write because item can
         * only be seen after publication via casNext.
         */
        Node(E item) {
            UNSAFE.putObject(this, itemOffset, item);
        }

        boolean casItem(E cmp, E val) {
            return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
        }

        void lazySetNext(Node<E> val) {
            UNSAFE.putOrderedObject(this, nextOffset, val);
        }

        boolean casNext(Node<E> cmp, Node<E> val) {
            return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
        }

        // Unsafe mechanics

        private static final sun.misc.Unsafe UNSAFE;
        private static final long itemOffset;
        private static final long nextOffset;

        static {
            try {
                UNSAFE = sun.misc.Unsafe.getUnsafe();
                Class<?> k = Node.class;
                itemOffset = UNSAFE.objectFieldOffset
                    (k.getDeclaredField("item"));
                nextOffset = UNSAFE.objectFieldOffset
                    (k.getDeclaredField("next"));
            } catch (Exception e) {
                throw new Error(e);
            }
        }
    }

总结

总的来说,JDK类库里面使用较多一点,大部分场景直接使用JUC下面类似ConcurrentHashMap这些就够了,真遇到性能不满足的场景,再根据Profile的结果针对性优化。

Flag Counter

分布式消息队列实现概要

前言

消息队列允许应用之间通过发消息的方式异步通讯,简单来说,发送者和消费者的生产效率通常是不一致的,那么我们就需要一种抽象模型去解耦,因此这里就可以引入消息队列,将任务暂时写入消息中间件,待消费者慢慢处理。消息中间件目前已经有了很多选择,例如RocketMQ、Kafka、Pulsar等等,Message queue带来很多便利的同时,也引入了一些技术上的复杂性,就像一个黑盒子一样,如果不能理解其原理,碰到了问题查起来也很蛋疼,今天我们就来看看如何着手实现一个简单的消息队列

正文

首先我们看看Kafka以及RocketMQ的包结构,看看一个分布式消息队列究竟需要哪些组件
image
image

存储层

消息队列最核心的组件之一就是存储层,消息如何落地、如何读取,这里的技术选型是比较重要的一点,例如RocketMQ以及Kafka都是选择存储到本机,也就是本地文件系统,而Pulsar则是选择存储到分布式文件系统bookKeeper中,当然也有一些选择了分布式KV系统甚至是数据库,例如Redis自身也是支持publish/consume模型的,具体的选择哪一种实现方式只要还是看自己的业务场景,例如如果可靠性要求较高但对性能并不那么敏感的场景可以选择数据库作为存储介质。
选择本地文件系统去实现一个分布式消息队列相对来说是这几种最复杂的,不仅仅需要自己实现文件的IO细节,对于复制、一致性(当出现网络异常或者系统异常宕机时如何根据日志恢复系统的状态)也都需要自己实现,而这每一部分都需要相当一部分精力去研究,我们这次只是先首先一个比较简单的原型,对于这个方案之后有时间会搞。
基于分布式KV的方案相对来说也是不错的方案,性能很不错,而且接口也比较人性化,但是可靠性差了一点,对于类似交易、缓存同步这种对可靠性要求比较高的场景来说不那么适用。
image

基于数据库的方式性能上会有很大的损失,DB的数据结构本质上就不适合去实现消息队列,速度和一致性只能选择一个。这次我们选择利用分布式文件系统作为存储介质,例如HDFS、Apache BookKeeper等,我们分析一下Message queue的场景,单线程写-多线程读,这里需要引入topic分区的概念,一般如果某些topic比较活跃,吞吐量比较高,那么我们可以将消息分区,实现思路一般是将topic再从细粒度切分为子topic,并将每个子topic分布到不同的broker上,从而实现性能的线性提升,也就是说这里的单线程写具体指的是单个分区,多线程读相对来说比较容易理解,而HDFS正好适合这个场景,而且我们也不用去管replica、写分片、刷盘策略等等,减少了很多实现的复杂性,BookKeeper在这方面是不错的选择。
image

客户端API实现

对于使用者而言,接触到的更多的是客户端暴漏的API,而客户端和服务器端Broker也需要一种方式通讯,对于RocketMQ以及Kafka都是选择实现了自定义的协议,消息队列的如果想要达到极高的吞吐量,实现一种高性能的网络通讯框架是相当重要的一环,RocketMQ是基于Netty之上构建的,而Kafka是直接基于NIO实现的,相对来说要复杂一点,如果看过源码的话会有所了解,Kafka客户端提交之后是先放到一个本地队列,然后根据broker、topic、分区信息等合并提交到服务器端,而Pulsar印象中是基于Protocol buffer实现的,这样相对自定义协议很多好处,首先如果协议后期实现过程有变动的话,如何兼容老的协议等这些细节已经由Protocol buffer帮你解决了,另外很重要的一点是,Protocol buffer可以帮你生成各个不同语言的API,如果是自定义协议这个又要费相当的精力去实现。

一致性

对于消息队列的场景,每条消息都是一旦落盘之后,就不再支持更新操作,对于读取也都是顺序读,consumer抓取到的消息也都是已经落盘的或者已经commit的记录,因此一致性在消息队列中相对来说还是比较容易实现的。

高可用

首先就存储层来说,我们的技术选型就已经决定了本质上就是高可用的,因为BookKeeper本身就支持指定复制到几个slave以及ack的机制,例如需要写入到所有的分区才向客户端返回成功,而对于broker端,因为我们的消息队列是存储和计算分离的,也就是说broker本身是无状态的,当producer/consumer连接的broker宕机或者网络超时的断开连接时,可以直接由另一个broker接着提供服务,当然这里还有很多细节问题,但是复杂性相对RocketMQ等已经降低了很多。

消费者进度存储

我们知道消息存在三种语义: at most once、at least once、exactly once,那么消费者offset的存储于同步机制就一定程度上决定了我们具体是什么语义,例如发送端,如果发送失败不重试的话就是 at most once,如果发送失败选择一定次数的重试,那么就是at least once,这里就可能造成消息重复落盘从而造成重复消费,例如说消息实际已经落盘但是发送提交响应的过程出现了网络异常,就会出现这种情况,而exactly once的场景就会比较复杂一点。我们回到offset的场景,RocketMQ以及Kafka默认都是定时去同步当前的消费进度,那么这个消费进度存储到哪里又是一个问题。
RocketMQ的方式是存储到本地文件系统中,Kafka在0.8版本之前是选择存储到了Zookeeper中,后面改成存储到另外一个topic中,那么这两种方式有什么优缺点呢:

  1. 性能/横向扩展: Zookeeper是一个一致性系统,它保留的API也都是基于key/value的格式,ZK本质上是不支持大量写的,同时ZK不支持横向扩展,因为每个节点都会同步所有的transaction 并保持整个数据集,实际上ZK是基于单个日志写并同步复制到其他节点的分布式系统。ZK的吞吐量据我测试差不多1W/S写左右,但是假如说我们有几十上百万个topic,每秒同步一次消费进度,这个时候ZK已经完全不能满足需要,而且并不能横向扩展,只能通过分片的方式解决,而这又引入了一个代理层
  2. 实现的复杂性: 基于本地文件系统性能虽然可观,但是和消息存储同理,需要考虑很多实现的细节,例如为了保证高可用,我们还需要考虑如何将本地offet快照文件同步到其他备机。

因此我们这里参考Kafka最新的实现,我们选择将消费进度也存储到BookKeeper中,这样就可以支持大量的写,而且支持线性扩展,BK也会将小的log合并存储到一个文件中,避免了性能被一些不活跃的topic所影响。

总结

本文简单讲解了实现一个分布式消息队列所需要考虑的一些方面,例如一致性、高可用、消费语义、通讯模型等等,但实际去写一个Message queue 所需要的远远不止这些,建议先从源码阅读开始,先理清整体的架构、脉络,再去研究细节、看代码,最好将每个项目的源码在IDE中实际的去打断点、调试,一步一步的了解到从发送消息到接收到消息的这一整个过程都发生了什么。

Flag Counter

初探JDBC源码

JDBC Driver注册

JDBC的核心接口之一是java.sql.Driver,每一个驱动都必须提供实现类,那么它是怎样和DriverManager一起为我们提供数据库连接服务的呢,首先看一段经典的连接JDBC的代码.

//Class.forName("com.mysql.jdbc.Driver") JDBC4不加这行代码也可以
 Connection con = DriverManager.getConnection(
                         "jdbc:mysql:///test",
                         username,
                         password);

那么这段代码究竟做了什么神奇的操作呢,我们直接进去DriverManager这个类一看究竟,可以看到构造器是私有的,从而阻止我们去初始化这个类,JVM加载这个类之后首先会调用它的static代码段,
DriverManager的static段调用了loadInitialDrivers()方法。

    private DriverManager(){}

    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
private static void loadInitialDrivers() {
        String drivers;
        //第一部分
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
        //第二部分
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });
            
        println("DriverManager.initialize: jdbc.drivers = " + drivers);
        //第三部分
        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }

第一部分

这段代码首先会去获取jdbc.drivers这个系统属性,可以通过

-Djdbc.drivers=com.mysql.jdbc.Driver

或者在代码中显示设置

System.setProperty("jdbc.drivers","com.mysql.jdbc.Driver");

然后会通过ServiceLoader加载驱动程序,在JDBC4中,驱动程序必须在META-INF/services/包含java.sql.Driver这个文件,在其中包含数据库驱动的实现类,
比如Mysql的中包含的内容是:

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver

第二部分

然后会加载找到的所有驱动程序,这个时候会调用驱动程序的static代码块,我们去看一下Mysql和H2数据库的Driver实现类在其中做了什么操作:

public class com.mysql.jdbc.Driver extends NonRegisteringDriver implements java.sql.Driver {

    static {
        try {
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }
public class org.h2.Driver implements java.sql.Driver {
     private static final Driver INSTANCE = new Driver();
     static {
         load();
     }
     public static synchronized Driver load() {
        try {
            if (!registered) {
                registered = true;
                DriverManager.registerDriver(INSTANCE);
            }
        } catch (SQLException e) {
            DbException.traceThrowable(e);
        }
        return INSTANCE;
    }

可以看到其中的共同点是都调用了DriverManager.registerDriver()去注册自己,其做的操作是将Driver封装成DriverInfo放到一个列表中保存。

    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
    
    public static synchronized void registerDriver(java.sql.Driver driver)
        throws SQLException {

        registerDriver(driver, null);
    }
    
    public static synchronized void registerDriver(java.sql.Driver driver,
            DriverAction da)
        throws SQLException {

        /* Register the driver if it has not already been added to our list */
        if(driver != null) {
            registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
        } else {
            // This is for compatibility with the original DriverManager
            throw new NullPointerException();
        }

        println("registerDriver: " + driver);

    }

第三部分

如果判断drivers为空,说明没有通过jdbc.drivers找到驱动类,接着有没有通过ServiceLoader找到驱动类并初始化都无所谓了,直接返回即可,如果drivers不为空,则通过Class.forName()去加载驱动类,
调用static代码块去注册自己,同时通过String[] driversList = drivers.split(":");可以看出,指定jdbc.drivers指定驱动时可以给出多个,用:分隔。

获取连接

经过上面这段代码,DriverManager已经初始化完毕,各个驱动也已经注册完成,接着就调用getConnection()去获取连接,其核心代码也很容易理解,就是调用connect()方法去建立连接

        for(DriverInfo aDriver : registeredDrivers) {
            // If the caller does not have permission to load the driver then
            // skip it.
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                try {
                    println("    trying " + aDriver.driver.getClass().getName());
                    Connection con = aDriver.driver.connect(url, info);
                    if (con != null) {
                        // Success!
                        println("getConnection returning " + aDriver.driver.getClass().getName());
                        return (con);
                    }
                } catch (SQLException ex) {
                    if (reason == null) {
                        reason = ex;
                    }
                }

            } else {
                println("    skipping: " + aDriver.getClass().getName());
            }

        }

Flag Counter

Gossip协议在Cassandra中的实现

Gossip协议是什么?

​ 简单来说就是一种去中心化、点对点的数据广播协议,你可以把它理解为病毒的传播。A传染给B,B继续传染给C,如此下去。

​ 协议本身只有一些简单的限制,状态更新的时间随着参与主机数的增长以对数的速率增长,即使是一些节点挂掉或者消息丢失也没关系。很多的分布式系统都用gossip 协议来解决自己遇到的一些难题。比如说服务发现框架consul就用了gossip协议( Serf)来做管理主机的关系以及集群之间的消息广播,Cassandra也用到了这个协议,用来实现一些节点发现、健康检查等。

通信流程

概述

首先系统需要配置几个种子节点,比如说A、B, 每个参与的节点都会维护所有节点的状态,node->(Key,Value,Version),版本号较大的说明其数据较新,节点P只能直接更新它自己的状态,节点P只能间接的通过gossip协议来更新本机维护的其他节点的数据。

大致的过程如下,

​ ① SYN:节点A向随机选择一些节点,这里可以只选择发送摘要,即不发送valus,避免消息过大

​ ② ACK:节点B接收到消息后,会将其与本地的合并,这里合并采用的是对比版本,版本较大的说明数据较新. 比如节点A向节点B发送数据C(key,value,2),而节点B本机存储的是C(key,value1,3),那么因为B的版本比较新,合并之后的数据就是B本机存储的数据,然后会发回A节点。

​ ③ ACK2:节点A接收到ACK消息,将其应用到本机的数据中

AGossipDigestSyn  => B执行GossipDigestSynVerbHandler 
BGossipDigestAck  => A执行GossipDigestAckVerbHandler 
AGossipDigestAck2 => B执行GossipDigestAck2VerbHandler

这三个类都实现了IVerbHandler接口,注册到MessagingService的处理器中:

MessagingService.instance().registerVerbHandlers(MessagingService.Verb.GOSSIP_DIGEST_SYN, new GossipDigestSynVerbHandler());
MessagingService.instance().registerVerbHandlers(MessagingService.Verb.GOSSIP_DIGEST_ACK, new GossipDigestAckVerbHandler());
MessagingService.instance().registerVerbHandlers(MessagingService.Verb.GOSSIP_DIGEST_ACK2, new GossipDigestAck2VerbHandler());

这样当消息模块接收到消息后就会调用对应的Handler处理,如下面的代码所示:

IVerbHandler verbHandler = MessagingService.instance().getVerbHandler(verb);
        if (verbHandler == null)
        {
          	//未知的消息不处理
            logger.trace("Unknown verb {}", verb);
            return;
        }

        try
        {
            verbHandler.doVerb(message, id);
        }
        catch (IOException ioe)
        {
            handleFailure(ioe);
            throw new RuntimeException(ioe);
        }
        catch (TombstoneOverwhelmingException | IndexNotAvailableException e)
        {
            handleFailure(e);
            logger.error(e.getMessage());
        }
        catch (Throwable t)
        {
            handleFailure(t);
            throw t;
        }

源码解析

初始化

具体的初始化都是在org.apache.cassandra.service.StorageService#public synchronized void initServer() throws ConfigurationException()去做的,里面会调用prepareToJoin() 尝试加入gossip集群。

private void prepareToJoin() throws ConfigurationException
    {
  		//volatile修饰保证可见性,已经加入了集群就直接跳过
        if (!joined)
        {
            /*....省略...*/
            if (!MessagingService.instance().isListening())
              	//开始监听消息
                MessagingService.instance().listen();
			
          	//给本节点起个名字
            UUID localHostId = SystemKeyspace.getLocalHostId();
			
          	/*
          	*  一次shadow round会获取所有到与之通讯节点拥有的所有节点的信息
          	*/
            if (replacing)
            {
                localHostId = prepareForReplacement();
                appStates.put(ApplicationState.TOKENS, valueFactory.tokens(bootstrapTokens));

                if (!DatabaseDescriptor.isAutoBootstrap())
                {
                    // Will not do replace procedure, persist the tokens we're taking over locally
                    // so that they don't get clobbered with auto generated ones in joinTokenRing
                    SystemKeyspace.updateTokens(bootstrapTokens);
                }
                else if (isReplacingSameAddress())
                {
                    //only go into hibernate state if replacing the same address (CASSANDRA-8523)
                    logger.warn("Writes will not be forwarded to this node during replacement because it has the same address as " +
                                "the node to be replaced ({}). If the previous node has been down for longer than max_hint_window_in_ms, " +
                                "repair must be run after the replacement process in order to make this node consistent.",
                                DatabaseDescriptor.getReplaceAddress());
                    appStates.put(ApplicationState.STATUS, valueFactory.hibernate(true));
                }
            }
            else
            {
                checkForEndpointCollision(localHostId);
            }

            // have to start the gossip service before we can see any info on other nodes.  this is necessary
            // for bootstrap to get the load info it needs.
            // (we won't be part of the storage ring though until we add a counterId to our state, below.)
            // Seed the host ID-to-endpoint map with our own ID.
            getTokenMetadata().updateHostId(localHostId, FBUtilities.getBroadcastAddress());
            appStates.put(ApplicationState.NET_VERSION, valueFactory.networkVersion());
            appStates.put(ApplicationState.HOST_ID, valueFactory.hostId(localHostId));
            appStates.put(ApplicationState.RPC_ADDRESS, valueFactory.rpcaddress(FBUtilities.getBroadcastRpcAddress()));
            appStates.put(ApplicationState.RELEASE_VERSION, valueFactory.releaseVersion());

            // load the persisted ring state. This used to be done earlier in the init process,
            // but now we always perform a shadow round when preparing to join and we have to
            // clear endpoint states after doing that.
            loadRingState();

            logger.info("Starting up server gossip");
          	//启动gossip,比如定时任务等
            Gossiper.instance.register(this);
            Gossiper.instance.start(SystemKeyspace.incrementAndGetGeneration(), appStates); // needed for node-ring gathering.
            gossipActive = true;
            // gossip snitch infos (local DC and rack)
            gossipSnitchInfo();
            // gossip Schema.emptyVersion forcing immediate check for schema updates (see MigrationManager#maybeScheduleSchemaPull)
            Schema.instance.updateVersionAndAnnounce(); // Ensure we know our own actual Schema UUID in preparation for updates
            LoadBroadcaster.instance.startBroadcasting();
            HintsService.instance.startDispatch();
            BatchlogManager.instance.start();
        }
    }


public synchronized Map<InetAddress, EndpointState> doShadowRound()
    {
        buildSeedsList();
        // it may be that the local address is the only entry in the seed
        // list in which case, attempting a shadow round is pointless
        if (seeds.isEmpty())
            return endpointShadowStateMap;

        seedsInShadowRound.clear();
        endpointShadowStateMap.clear();
        // 构造一个空的Syn消息,表明这是一次shadow round
        List<GossipDigest> gDigests = new ArrayList<GossipDigest>();
        GossipDigestSyn digestSynMessage = new GossipDigestSyn(DatabaseDescriptor.getClusterName(),
                DatabaseDescriptor.getPartitionerName(),
                gDigests);
        MessageOut<GossipDigestSyn> message = new MessageOut<GossipDigestSyn>(MessagingService.Verb.GOSSIP_DIGEST_SYN,
                digestSynMessage,
                GossipDigestSyn.serializer);

        inShadowRound = true;
        int slept = 0;
        try
        {
            while (true)
            {	
              	/*
              	*  第一次以及后面每五秒都会尝试向所有的种子节点发送一次shdow round syn消息,尝试
              	*  获取所有的节点的信息。如果达到了最大的延迟(默认为30S)或者已经达到了目的就会退出
              	*/
                if (slept % 5000 == 0)
                { 
                    logger.trace("Sending shadow round GOSSIP DIGEST SYN to seeds {}", seeds);

                    for (InetAddress seed : seeds)
                        MessagingService.instance().sendOneWay(message, seed);
                }

                Thread.sleep(1000);
                if (!inShadowRound)
                    break;

                slept += 1000;
                if (slept > StorageService.RING_DELAY)
                {
                    // if we don't consider ourself to be a seed, fail out
                    if (!DatabaseDescriptor.getSeeds().contains(FBUtilities.getBroadcastAddress()))
                        throw new RuntimeException("Unable to gossip with any seeds");

                    logger.warn("Unable to gossip with any seeds but continuing since node is in its own seed list");
                    inShadowRound = false;
                    break;
                }
            }
        }
        catch (InterruptedException wtf)
        {
            throw new RuntimeException(wtf);
        }

        return ImmutableMap.copyOf(endpointShadowStateMap);
    }

Gossiper#start()中启动一个定时任务GossipTask,默认为每秒一次,发送SYN消息:

/*
* 线程池最好都指定名字,这样方便查问题,另外最好指定好队列大小,最好不要用Executors中
* 默认的无界队列,关闭的时候注意处理好中断,很多人都是catch Exception后打个异常就算了,
* 这样不是很好的处理方式,我个人通常是当catch到InterruptedException后,根据业务场景决定是 
* 需要通过interrupt方法重置中断位,当处理完这轮任务之后,决定是否退出
*/
private static final DebuggableScheduledThreadPoolExecutor executor = new DebuggableScheduledThreadPoolExecutor("GossipTasks");

public void start(int generationNbr, Map<ApplicationState, VersionedValue> preloadLocalStates)
    {
        buildSeedsList();
        /* initialize the heartbeat state for this localEndpoint */
        maybeInitializeLocalState(generationNbr);
        EndpointState localState = endpointStateMap.get(FBUtilities.getBroadcastAddress());
        localState.addApplicationStates(preloadLocalStates);

        //notify snitches that Gossiper is about to start
        DatabaseDescriptor.getEndpointSnitch().gossiperStarting();
        if (logger.isTraceEnabled())
            logger.trace("gossip started with generation {}", localState.getHeartBeatState().getGeneration());

        scheduledGossipTask = executor.scheduleWithFixedDelay(new GossipTask(),
                                                              Gossiper.intervalInMillis,
                                                              Gossiper.intervalInMillis,
                                                              TimeUnit.MILLISECONDS);
    }

那么GossipTask内部的实现是怎样的呢?

  private class GossipTask implements Runnable
    {
        public void run()
        {
            try
            {
                //等待MessagingService开始监听
                MessagingService.instance().waitUntilListening();
	        //加锁
                taskLock.lock();
 
	    //更新心跳计数器,这个是用来做失败检测的,这里会有个定时任务轮询这个Map,检测最 近一次的
             //心跳时间,如果距离当前时间差距不合理,那么我们就可以认为这个节点挂掉了,可以放到另外
             //队列中,随后隔一段时间再去看看是否恢复。
             endpointStateMap.get(FBUtilities.getBroadcastAddress()).getHeartBeatState()
                                              .updateHeartBeat();
                if (logger.isTraceEnabled())
                    logger.trace("My heartbeat is now {}", endpointStateMap.get(FBUtilities.getBroadcastAddress()).getHeartBeatState().getHeartBeatVersion());
                final List<GossipDigest> gDigests = new ArrayList<GossipDigest>();
                //随机选择一些节点,构造摘要列表
              	Gossiper.instance.makeRandomGossipDigest(gDigests);

                if (gDigests.size() > 0)
                {
                  	//构造消息,可以看到这里的类型是GOSSIP_DIGEST_SYN
                    GossipDigestSyn digestSynMessage = new GossipDigestSyn(DatabaseDescriptor.getClusterName(),
                                                                           DatabaseDescriptor.getPartitionerName(),
                                                                           gDigests);
                    MessageOut<GossipDigestSyn> message = new MessageOut<GossipDigestSyn>(MessagingService.Verb.GOSSIP_DIGEST_SYN,
                                                                                          digestSynMessage,
                                                                                          GossipDigestSyn.serializer);
                  	/*将消息发送给一个活着的节点,随机选择的,代码如下
                  	*  int index = (size == 1) ? 0 : random.nextInt(size);
				    *  InetAddress to = liveEndpoints.get(index);
				    *  如果选择到的是种子节点,那么就会返回true.
                  	*/ 
                    boolean gossipedToSeed = doGossipToLiveMember(message);
                    //随机决定是否向挂掉的节点发送gossip消息
                  	maybeGossipToUnreachableMember(message);
                  	/*
                  	* 可参见这个issue:https://issues.apache.org/jira/browse/CASSANDRA-150
                  	*/
                    if (!gossipedToSeed || liveEndpoints.size() < seeds.size())
                        maybeGossipToSeed(message);
						 doStatusCheck();
                }
            }
            catch (Exception e)
            {
                JVMStabilityInspector.inspectThrowable(e);
                logger.error("Gossip error", e);
            }
            finally
            {
                taskLock.unlock();
            }
        }
    }

GossipDigestSynVerbHandler

public void doVerb(MessageIn<GossipDigestSyn> message, int id)
    {
        InetAddress from = message.from;
        if (logger.isTraceEnabled())
            logger.trace("Received a GossipDigestSynMessage from {}", from);
        if (!Gossiper.instance.isEnabled() && !Gossiper.instance.isInShadowRound())
        {
            if (logger.isTraceEnabled())
                logger.trace("Ignoring GossipDigestSynMessage because gossip is disabled");
            return;
        }

        GossipDigestSyn gDigestMessage = message.payload;
        /* 不是同一个集群的就不处理 */
        if (!gDigestMessage.clusterId.equals(DatabaseDescriptor.getClusterName()))
        {
            logger.warn("ClusterName mismatch from {} {}!={}", from, gDigestMessage.clusterId, DatabaseDescriptor.getClusterName());
            return;
        }

        if (gDigestMessage.partioner != null && !gDigestMessage.partioner.equals(DatabaseDescriptor.getPartitionerName()))
        {
            logger.warn("Partitioner mismatch from {} {}!={}", from, gDigestMessage.partioner, DatabaseDescriptor.getPartitionerName());
            return;
        }

        List<GossipDigest> gDigestList = gDigestMessage.getGossipDigests();

        /*发送者和接受者都处于shadow round阶段,那么就发送一个空的ack回去*/
        if (!Gossiper.instance.isEnabled() && Gossiper.instance.isInShadowRound())
        {
            // a genuine syn (as opposed to one from a node currently
            // doing a shadow round) will always contain > 0 digests
            if (gDigestList.size() > 0)
            {
                logger.debug("Ignoring non-empty GossipDigestSynMessage because currently in gossip shadow round");
                return;
            }

            logger.debug("Received a shadow round syn from {}. Gossip is disabled but " +
                         "currently also in shadow round, responding with a minimal ack", from);
                // new ArrayList<>默认16的size,也会占用额外的内存,
          	// 可以考虑改成0或者使用Collections.EMPTY_LIST
          	MessagingService.instance()
                            .sendOneWay(new MessageOut<>(MessagingService.Verb.GOSSIP_DIGEST_ACK,
                                                         new GossipDigestAck(new ArrayList<>(), new HashMap<>()),
                                                         GossipDigestAck.serializer),
                                        from);
            return;
        }

        if (logger.isTraceEnabled())
        {
            StringBuilder sb = new StringBuilder();
            for (GossipDigest gDigest : gDigestList)
            {
                sb.append(gDigest);
                sb.append(" ");
            }
            logger.trace("Gossip syn digests are : {}", sb);
        }
		
  		/*
  		* 下面的工作其实就类似于git中的merge,如上文所说,版本大的说明他所持有的节点信息较新
  		* 这里就是做一个diff,如果你的version比我本地的大,那么我就发一个请求,让你把这个节点的
  		* 信息发给我,如果我的version比你的大,那么说明我的信息更新一点,就会告诉你,你的该更
                * 新了然后就会发一个GossipDigestAck消息回去。
  		*/
        doSort(gDigestList);

        List<GossipDigest> deltaGossipDigestList = new ArrayList<GossipDigest>();
        Map<InetAddress, EndpointState> deltaEpStateMap = new HashMap<InetAddress, EndpointState>();
        Gossiper.instance.examineGossiper(gDigestList, deltaGossipDigestList, deltaEpStateMap);
        logger.trace("sending {} digests and {} deltas", deltaGossipDigestList.size(), deltaEpStateMap.size());
        MessageOut<GossipDigestAck> gDigestAckMessage = new MessageOut<GossipDigestAck>(MessagingService.Verb.GOSSIP_DIGEST_ACK,
                                                                                        new GossipDigestAck(deltaGossipDigestList, deltaEpStateMap),
                                                                                        GossipDigestAck.serializer);
        if (logger.isTraceEnabled())
            logger.trace("Sending a GossipDigestAckMessage to {}", from);
        MessagingService.instance().sendOneWay(gDigestAckMessage, from);
    }

核心的实现:

void examineGossiper(List<GossipDigest> gDigestList, List<GossipDigest> deltaGossipDigestList, Map<InetAddress, EndpointState> deltaEpStateMap)
    {
        if (gDigestList.size() == 0)
        {
          
           /* 
            * 如果是空的,表明这是一次shadow round,那么我们要把自己所有已知的节点信息发过去。
            */
            logger.debug("Shadow request received, adding all states");
            for (Map.Entry<InetAddress, EndpointState> entry : endpointStateMap.entrySet())
            {
                gDigestList.add(new GossipDigest(entry.getKey(), 0, 0));
            }
        }
        for ( GossipDigest gDigest : gDigestList )
        {
            int remoteGeneration = gDigest.getGeneration();
            int maxRemoteVersion = gDigest.getMaxVersion();
            /* Get state associated with the end point in digest */
            EndpointState epStatePtr = endpointStateMap.get(gDigest.getEndpoint());
            /*
                Here we need to fire a GossipDigestAckMessage. If we have some data associated with this endpoint locally
                then we follow the "if" path of the logic. If we have absolutely nothing for this endpoint we need to
                request all the data for this endpoint.
            */
            if (epStatePtr != null)
            {
                int localGeneration = epStatePtr.getHeartBeatState().getGeneration();
                /* get the max version of all keys in the state associated with this endpoint */
                int maxLocalVersion = getMaxEndpointStateVersion(epStatePtr);
                if (remoteGeneration == localGeneration && maxRemoteVersion == maxLocalVersion)
                    continue;

                if (remoteGeneration > localGeneration)
                {
                    /* we request everything from the gossiper */
                    requestAll(gDigest, deltaGossipDigestList, remoteGeneration);
                }
                else if (remoteGeneration < localGeneration)
                {
                    /* send all data with generation = localgeneration and version > 0 */
                    sendAll(gDigest, deltaEpStateMap, 0);
                }
                else if (remoteGeneration == localGeneration)
                {
                    /*
                        If the max remote version is greater then we request the remote endpoint send us all the data
                        for this endpoint with version greater than the max version number we have locally for this
                        endpoint.
                        If the max remote version is lesser, then we send all the data we have locally for this endpoint
                        with version greater than the max remote version.
                    */
                    if (maxRemoteVersion > maxLocalVersion)
                    {
                        deltaGossipDigestList.add(new GossipDigest(gDigest.getEndpoint(), remoteGeneration, maxLocalVersion));
                    }
                    else if (maxRemoteVersion < maxLocalVersion)
                    {
                        /* send all data with generation = localgeneration and version > maxRemoteVersion */
                        sendAll(gDigest, deltaEpStateMap, maxRemoteVersion);
                    }
                }
            }
            else
            {
                /* We are here since we have no data for this endpoint locally so request everything. */
                requestAll(gDigest, deltaGossipDigestList, remoteGeneration);
            }
        }
    }

GossipDigestAckVerbHandler

public void doVerb(MessageIn<GossipDigestAck> message, int id)
    {
        InetAddress from = message.from;
        if (logger.isTraceEnabled())
            logger.trace("Received a GossipDigestAckMessage from {}", from);
        if (!Gossiper.instance.isEnabled() && !Gossiper.instance.isInShadowRound())
        {
            if (logger.isTraceEnabled())
                logger.trace("Ignoring GossipDigestAckMessage because gossip is disabled");
            return;
        }

        GossipDigestAck gDigestAckMessage = message.payload;
        List<GossipDigest> gDigestList = gDigestAckMessage.getGossipDigestList();
        Map<InetAddress, EndpointState> epStateMap = gDigestAckMessage.getEndpointStateMap();
        logger.trace("Received ack with {} digests and {} states", gDigestList.size(), epStateMap.size());

        if (Gossiper.instance.isInShadowRound())
        {
            if (logger.isDebugEnabled())
                logger.debug("Received an ack from {}, which may trigger exit from shadow round", from);

            // 如果是空的,说明他也在shdow round中,木有事,反正还会重试的
            Gossiper.instance.maybeFinishShadowRound(from, gDigestList.isEmpty() && epStateMap.isEmpty(), epStateMap);
            return; 
        }

        if (epStateMap.size() > 0)
        {
            /*
            * 第一次发送SYN消息的时候会更新firstSynSendAt,如果ACK消息
            * 是在我们第一次SYN之前的,那么说明这个ACK已经过期了,直接忽略。
            */
            if ((System.nanoTime() - Gossiper.instance.firstSynSendAt) < 0 || Gossiper.instance.firstSynSendAt == 0)
            {
                if (logger.isTraceEnabled())
                    logger.trace("Ignoring unrequested GossipDigestAck from {}", from);
                return;
            }

            /* 失败检测相关的,先不管 */
            Gossiper.instance.notifyFailureDetector(epStateMap);
          	/*将远程收到的信息跟本地的merge,类似上面的操作*/
            Gossiper.instance.applyStateLocally(epStateMap);
        }

        /*
        * 构造一个GossipDigestAck2Message消息,将对方需要的节点信息发给他
        */
        Map<InetAddress, EndpointState> deltaEpStateMap = new HashMap<InetAddress, EndpointState>();
        for (GossipDigest gDigest : gDigestList)
        {
            InetAddress addr = gDigest.getEndpoint();
            EndpointState localEpStatePtr = Gossiper.instance.getStateForVersionBiggerThan(addr, gDigest.getMaxVersion());
            if (localEpStatePtr != null)
                deltaEpStateMap.put(addr, localEpStatePtr);
        }

        MessageOut<GossipDigestAck2> gDigestAck2Message = new MessageOut<GossipDigestAck2>(MessagingService.Verb.GOSSIP_DIGEST_ACK2,
                                                                                           new GossipDigestAck2(deltaEpStateMap),
                                                                                           GossipDigestAck2.serializer);
        if (logger.isTraceEnabled())
            logger.trace("Sending a GossipDigestAck2Message to {}", from);
        MessagingService.instance().sendOneWay(gDigestAck2Message, from);
    }

GossipDigestAck2VerbHandler

    public void doVerb(MessageIn<GossipDigestAck2> message, int id)
    {
        if (logger.isTraceEnabled())
        {
            InetAddress from = message.from;
            logger.trace("Received a GossipDigestAck2Message from {}", from);
        }
        if (!Gossiper.instance.isEnabled())
        {
            if (logger.isTraceEnabled())
                logger.trace("Ignoring GossipDigestAck2Message because gossip is disabled");
            return;
        }
        Map<InetAddress, EndpointState> remoteEpStateMap = message.payload.getEndpointStateMap();
        Gossiper.instance.notifyFailureDetector(remoteEpStateMap);
      	/*将收到的节点信息与本地的merge*/
        Gossiper.instance.applyStateLocally(remoteEpStateMap);
    }

总结

源码上看结构是非常清晰的,每一步的逻辑相对来讲还是比较容易理解的,其实也就类似tcp三次握手:

①、A随机找个人B,随机告诉他一些我知道的信息(这里可以根据时间排序、根据版本打分等等,具体可以参照论文)

②、B收到以后,和自己本地对比下,比A新的发回给A,比A旧的让通知A在下一步告诉我

③、A本地合并下,然后将B需要的信息告诉他

④、B本地合并下

⑤、完成了

参考资料

  1. https://www.cs.cornell.edu/home/rvr/papers/flowgossip.pdf
  2. https://www.consul.io
  3. https://www.serf.io/
  4. https://en.wikipedia.org/wiki/Gossip_protocol
  5. https://github.com/apache/cassandra

Flag Counter

SpringMVC校验机制参数顺序的坑

问题

今天遇到一个小问题,在进行表单提交之后,直接进入了400错误页面,这个比较诡异,我所做的无非就是进行了简单的参数验证,提取BindingResult中的信息放到Model中方便前台显示,如下:

@RequestMapping(value = "/user/publish",method = RequestMethod.POST)
String publish(@Valid TopicForm topicForm,Model model,BindingResult result){
    if(result.hasErrors()){
        model.addAttribute("errors",result.allErrors)
        return "/user/publish"
    }
    Set<Tag> tagSet = tagService.constructeTags(topicForm.tags)
        topicService.publish(topicForm.build(tagSet))
    return "redirect:/"
}

解决过程

@Canonical
class TopicForm {


    @NotEmpty(message = "标题不能为空")
    @Length(min = 6, max = 125, message = "标题最少6个字符")
    String title

    @NotEmpty
    @Length(min = 15, max = 20, message = "内容必须在20-2W个字符哟")
    String content //故意改成20,产生验证错误

}

直接打断点,发现根本没进入这段代码,因此猜测是验证的时候报了异常,因此将@Lengthmax改成20,产生错误结果,然后进入org.hibernate.validator.internal.constraintvalidators.hv.LengthValidator,在isValid方法上右键Add to watches,打个断点(友情提示,本人用的IDEA-16),一步一步跟踪调试发现进入了下面这段关键的代码段:

public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {

public final Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

		String name = ModelFactory.getNameForParameter(parameter);
		Object attribute = (mavContainer.containsAttribute(name) ?
				mavContainer.getModel().get(name) : createAttribute(name, parameter, binderFactory, webRequest));

		WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
		if (binder.getTarget() != null) {
			bindRequestParameters(binder, webRequest);
			validateIfApplicable(binder, parameter);
			if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
				throw new BindException(binder.getBindingResult());
			}
		}

		// Add resolved attribute and BindingResult at the end of the model
		Map<String, Object> bindingResultModel = binder.getBindingResult().getModel();
		mavContainer.removeAttributes(bindingResultModel);
		mavContainer.addAllAttributes(bindingResultModel);

		return binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
	}
}

就是在这个地方抛出了一个BindException的异常,然后Spring进行了其他一些处理进去了400页面,不重要,我们看看这个判断条件,hasErrors()是用来判断是否有参数验证错误,这里很明显为true,下面还有个关键方法,我们进去一探究竟:

protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter methodParam) {
  int i = methodParam.getParameterIndex();
  Class<?>[] paramTypes = methodParam.getMethod().getParameterTypes();
  boolean hasBindingResult = (paramTypes.length > (i + 1) && Errors.class.isAssignableFrom(paramTypes[i + 1]));
  return !hasBindingResult;
}

这里一看就明晰了,getParameterIndex()获取的就是@Valid标注的方法参数索引,然会去判断紧跟其后的参数是否为Errors的子类,这时候我想到上面那个publish方法,我将Model作为其后续参数,而BindingResult为最后一个,因此肯定会返回true,导致抛出BindException异常。

总结

  1. 出了问题不要立马就去Google,先自己尝试去解决,打个断点进入源码调试下,进而分析问题可能产生的原因
  2. 平常注意看文档,对用到的东西要了若指掌

一文带你搞懂API网关

前言

假设你正在开发一个电商网站,那么这里会涉及到很多后端的微服务,比如会员、商品、推荐服务等等。

image

那么这里就会遇到一个问题,APP/Browser怎么去访问这些后端的服务? 如果业务比较简单的话,可以给每个业务都分配一个独立的域名(https://service.api.company.com),但这种方式会有几个问题:

  • 每个业务都会需要鉴权、限流、权限校验等逻辑,如果每个业务都各自为战,自己造轮子实现一遍,会很蛋疼,完全可以抽出来,放到一个统一的地方去做。
  • 如果业务量比较简单的话,这种方式前期不会有什么问题,但随着业务越来越复杂,比如淘宝、亚马逊打开一个页面可能会涉及到数百个微服务协同工作,如果每一个微服务都分配一个域名的话,一方面客户端代码会很难维护,涉及到数百个域名,另一方面是连接数的瓶颈,想象一下你打开一个APP,通过抓包发现涉及到了数百个远程调用,这在移动端下会显得非常低效。
  • 每上线一个新的服务,都需要运维参与,申请域名、配置Nginx等,当上线、下线服务器时,同样也需要运维参与,另外采用域名这种方式,对于环境的隔离也不太友好,调用者需要自己根据域名自己进行判断。
  • 另外还有一个问题,后端每个微服务可能是由不同语言编写的、采用了不同的协议,比如HTTP、Dubbo、GRPC等,但是你不可能要求客户端去适配这么多种协议,这是一项非常有挑战的工作,项目会变的非常复杂且很难维护。
  • 后期如果需要对微服务进行重构的话,也会变的非常麻烦,需要客户端配合你一起进行改造,比如商品服务,随着业务变的越来越复杂,后期需要进行拆分成多个微服务,这个时候对外提供的服务也需要拆分成多个,同时需要客户端配合你进行改造,非常蛋疼。

API Gateway

image

更好的方式是采用API网关,实现一个API网关接管所有的入口流量,类似Nginx的作用,将所有用户的请求转发给后端的服务器,但网关做的不仅仅只是简单的转发,也会针对流量做一些扩展,比如鉴权、限流、权限、熔断、协议转换、错误码统一、缓存、日志、监控、告警等,这样将通用的逻辑抽出来,由网关统一去做,业务方也能够更专注于业务逻辑,提升迭代的效率。
通过引入API网关,客户端只需要与API网关交互,而不用与各个业务方的接口分别通讯,但多引入一个组件就多引入了一个潜在的故障点,因此要实现一个高性能、稳定的网关,也会涉及到很多点。
image

API注册

业务方如何接入网关?一般来说有几种方式。

  • 第一种采用插件扫描业务方的API,比如Spring MVC的注解,并结合Swagger的注解,从而实现参数校验、文档&&SDK生成等功能,扫描完成之后,需要上报到网关的存储服务。

  • 手动录入。比如接口的路径、请求参数、响应参数、调用方式等信息,但这种方式相对来说会麻烦一些,如果参数过多的话,前期录入会很费时费力。
    image

  • 配置文件导入。比如通过Swagger\OpenAPI等,比如阿里云的网关:
    image

协议转换

内部的API可能是由很多种不同的协议实现的,比如HTTP、Dubbo、GRPC等,但对于用户来说其中很多都不是很友好,或者根本没法对外暴露,比如Dubbo服务,因此需要在网关层做一次协议转换,将用户的HTTP协议请求,在网关层转换成底层对应的协议,比如HTTP -> Dubbo, 但这里需要注意很多问题,比如参数类型,如果类型搞错了,导致转换出问题,而日志又不够详细的话,问题会很难定位。

服务发现

网关作为流量的入口,负责请求的转发,但首先需要知道转发给谁,如何寻址,这里有几种方式:

  • 写死在代码/配置文件里,这种方式虽然比较挫,但也能使用,比如线上仍然使用的是物理机,IP变动不会很频繁,但扩缩容、包括应用上下线都会很麻烦,网关自身甚至需要实现一套健康监测机制。
  • 域名。采用域名也是一种不错的方案,对于所有的语言都适用,但对于内部的服务,走域名会很低效,另外环境隔离也不太友好,比如预发、线上通常是同一个数据库,因此网关读取到的可能是同一个域名,这时候预发的网关调用的就是线上的服务。
  • 注册中心。采用注册中心就不会有上述的这些问题,即使是在容器环境下,节点的IP变更比较频繁,但节点列表的实时维护会由注册中心搞定,对网关是透明的,另外应用的正常上下线、包括异常宕机等情况,也会由注册中心的健康检查机制检测到,并实时反馈给网关。并且采用注册中心性能也没有额外的性能损耗,采用域名的方式,额外需要走一次DNS解析、Nginx转发等,中间多了很多跳,性能会有很大的下降,但采用注册中心,网关是和业务方直接点对点的通讯,不会有额外的损耗。

服务调用

网关由于对接很多种不同的协议,因此可能需要实现很多种调用方式,比如HTTP、Dubbo等,基于性能原因,最好都采用异步的方式,而Http、Dubbo都是支持异步的,比如apache就提供了基于NIO实现的异步HTTP客户端。
因为网关会涉及到很多异步调用,比如拦截器、HTTP客户端、dubbo、redis等,因此需要考虑下异步调用的方式,如果基于回调或者future的话,代码嵌套会很深,可读性很差,可以参考zuul和spring cloud gateway的方案,基于响应式进行改造。

优雅下线

优雅下线也是网关需要关注的一个问题,网关底层会涉及到很多种协议,比如HTTP、Dubbo,而HTTP又可以继续细分,比如域名、注册中心等,有些自身就支持优雅下线,比如Nginx自身是支持健康监测机制的,如果检测到某一个节点已经挂掉了,就会把这个节点摘掉,对于应用正常下线,需要结合发布系统,首先进行逻辑下线,然后对后续Nginx的健康监测请求直接返回失败(比如直接返回500),然后等待一段时间(根据Nginx配置决定),然后再将应用实际下线掉。另外对于注册中心的其实也类似,一般注册中心是只支持手动下线的,可以在逻辑下线阶段调用注册中心的接口将节点下线掉,而有些不支持主动下线的,需要结合缓存的配置,让应用延迟下线。另外对于其他比如Dubbo等原理也是类似。

性能

网关作为所有流量的入口,性能是重中之重,早期大部分网关都是基于同步阻塞模型构建的,比如Zuul 1.x。但这种同步的模型我们都知道,每个请求/连接都会占用一个线程,而线程在JVM中是一个很重的资源,比如Tomcat默认就是200个线程,如果网关隔离没有做好的话,当发生网络延迟、FullGC、第三方服务慢等情况造成上游服务延迟时,线程池很容易会被打满,造成新的请求被拒绝,但这个时候其实线程都阻塞在IO上,系统的资源被没有得到充分的利用。另外一点,容易受网络、磁盘IO等延迟影响。需要谨慎设置超时时间,如果设置不当,且服务隔离做的不是很完善的话,网关很容易被一个慢接口拖垮。

而异步化的方式则完全不同,通常情况下一个CPU核启动一个线程即可处理所有的请求、响应。一个请求的生命周期不再固定于一个线程,而是会分成不同的阶段交由不同的线程池处理,系统的资源能够得到更充分的利用。而且因为线程不再被某一个连接独占,一个连接所占用的系统资源也会低得多,只是一个文件描述符加上几个监听器等,而在阻塞模型中,每条连接都会独占一个线程,而线程是一个非常重的资源。对于上游服务的延迟情况,也能够得到很大的缓解,因为在阻塞模型中,慢请求会独占一个线程资源,而异步化之后,因为单条连接所占用的资源变的非常低,系统可以同时处理大量的请求。
如果是JVM平台,Zuul 2、Spring Cloud gateway等都是不错的异步网关选型,另外也可以基于Netty、Spring Boot2.x的webflux、vert.x或者servlet3.1的异步支持进行自研。

缓存

对于一些幂等的get请求,可以在网关层面根据业务方指定的缓存头做一层缓存,存储到Redis等二级缓存中,这样一些重复的请求,可以在网关层直接处理,而不用打到业务线,降低业务方的压力,另外如果业务方节点挂掉,网关也能够返回自身的缓存。

限流

限流对于每个业务组件来说,可以说都是一个必须的组件,如果限流做不好的话,当请求量突增时,很容易导致业务方的服务挂掉,比如双11、双12等大促时,接口的请求量是平时的数倍,如果没有评估好容量,又没有做限流的话,很容易服务整个不可用,因此需要根据业务方接口的处理能力,做好限流策略,相信大家都见过淘宝、百度抢红包时的降级页面。
因此一定要在接入层做好限流策略,对于非核心接口可以直接将降级掉,保障核心服务的可用性,对于核心接口,需要根据压测时得到的接口容量,制定对应的限流策略。限流又分为几种:

  • 单机。单机性能比较高,不涉及远程调用,只是本地计数,对接口RT影响最小。但需要考虑下限流数的设置,比如是针对单台网关、还是整个网关集群,如果是整个集群的话,需要考虑到网关缩容、扩容时修改对应的限流数。
  • 分布式。分布式的就需要一个存储节点维护当前接口的调用数,比如redis、sentinel等,这种方式由于涉及到远程调用,会有些性能损耗,另外也需要考虑到存储挂掉的问题,比如redis如果挂掉,网关需要考虑降级方案,是降级到本地限流,还是直接将限流功能本身降级掉。
    另外还有不同的策略:简单计数、令牌桶等,大部分场景下其实简单计数已经够用了,但如果需要支持突发流量等场景时,可以采用令牌桶等方案。还需要考虑根据什么限流,比如是IP、接口、用户维度、还是请求参数中的某些值,这里可以采用表达式,相对比较灵活。

稳定性

稳定性是网关非常重要的一环,监控、告警需要做的很完善才可以,比如接口调用量、响应时间、异常、错误码、成功率等相关的监控告警,还有线程池相关的一些,比如活跃线程数、队列积压等,还有些系统层面的,比如CPU、内存、FullGC这些基本的。
网关是所有服务的入口,对于网关的稳定性的要求相对于其他服务会更高,最好能够一直稳定的运行,尽量少重启,但当新增功能、或者加日志排查问题时,不可避免的需要重新发布,因此可以参考zuul的方式,将所有的核心功能都基于不同的拦截器实现,拦截器的代码采用Groovy编写,存储到数据库中,支持动态加载、编译、运行,这样在出了问题的时候能够第一时间定位并解决,并且如果网关需要开发新功能,只需要增加新的拦截器,并动态添加到网关即可,不需要重新发布。

熔断降级

熔断机制也是非常重要的一项。若某一个服务挂掉、接口响应严重超时等发生,则可能整个网关都被一个接口拖垮,因此需要增加熔断降级,当发生特定异常的时候,对接口降级由网关直接返回,可以基于Hystrix或者Resilience4j实现。

日志

由于所有的请求都是由网关处理的,因此日志也需要相对比较完善,比如接口的耗时、请求方式、请求IP、请求参数、响应参数(注意脱敏)等,另外由于可能涉及到很多微服务,因此需要提供一个统一的traceId方便关联所有的日志,可以将这个traceId置于响应头中,方便排查问题。

隔离

比如线程池、http连接池、redis等应用层面的隔离,另外也可以根据业务场景,将核心业务部署带单独的网关集群,与其他非核心业务隔离开。

网关管控平台

这块也是非常重要的一环,需要考虑好整个流程的用户体验,比如接入到网关的这个流程,能不能尽量简化、智能,比如如果是dubbo接口,我们可以通过到git仓库中获取源码、解析对应的类、方法,从而实现自动填充,尽量帮用户减少操作;另外接口一般是从测试->预发->线上,如果每次都要填写一遍表单会非常麻烦,我们能不能自动把这个事情做掉,另外如果网关部署到了多个可用区、甚至不同的国家,那这个时候,我们还需要接口数据同步功能,不然用户需要到每个后台都操作一遍,非常麻烦。
这块个人的建议是直接参考阿里云、aws等提供的网关服务即可,功能非常全面。

其他

其他还有些需要考虑到的点,比如接口mock,文档生成、sdk代码生成、错误码统一、服务治理相关的等,这里就不累述了。

总结

目前的网关还是中心化的架构,所有的请求都需要走一次网关,因此当大促或者流量突增时,网关可能会成为性能的瓶颈,而且当网关接入的大量接口的时候,做好流量评估也不是一项容易的工作,每次大促前都需要跟业务方一起针对接口做压测,评估出大致的容量,并对网关进行扩容,而且网关是所有流量的入口,所有的请求都是由网关处理,要想准确的评估出容量很复杂。可以参考目前比较流行的ServiceMesh,采用去中心化的方案,将网关的逻辑下沉到sidecar中,
sidecar和应用部署到同一个节点,并接管应用流入、流出的流量,这样大促时,只需要对相关的业务压测,并针对性扩容即可,另外升级也会更平滑,中心化的网关,即使灰度发布,但是理论上所有业务方的流量都会流入到新版本的网关,如果出了问题,会影响到所有的业务,但这种去中心化的方式,可以先针对非核心业务升级,观察一段时间没问题后,再全量推上线。另外ServiceMesh的方案,对于多语言支持也更友好。

基于JVM之上的并发编程模式剖析

并发编程的概念并不新鲜,每一种编程语言中都内置了相关的支持,而有些编程语言因为对并发提供了更有友好的支持而得到了更多的关注。

拥抱并发

使用并发编程并不仅仅是为了CPU多核从而使得程序能够并行执行,其本质其实就是为了消除延迟,例如访问硬盘、网络IO等慢速的设备相对单纯的CPU计算会有很高的延迟,进而导致线程阻塞在这里等待资源,这个时候CPU的资源就白白浪费了,因此我们会根据业务场景,选择开启多个线程,将这些比较耗时的IO任务丢到另外的线程中去处理,这样就不会因为某些慢请求而影响其他用户,从而提高响应时间。因此这里就涉及到了并发模型的选型,下面我选择几种并尝试总结其优劣。

Java Future

Java Future 代表一种异步计算的结果,调用方可以检查计算是否完成、等待计算完成、获取计算结果等,使用起来非常的直观,但当多个Future组合起来时,特别每个异步计算的耗时都不一样,代码通常会变的很复杂并且很容易出错。各种Future#gettry/catch充斥在代码中,导致可读性变的非常差。

    private static final ExecutorService executorService = Executors.newCachedThreadPool();

    public static void main(String[] args) {
        Future<List<String>> blogsFuture = fetchBlogs();
        try {
            List<String> blogs = blogsFuture.get();
            blogs.forEach(s -> {
                Future<String> imageFuture = fetchImage(s);
                try {
                    String image = imageFuture.get();
                } catch (InterruptedException | ExecutionException e) {
                    e.printStackTrace();
                }
            });
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }

    public static Future<List<String>> fetchBlogs() {
        return executorService.submit(
            (Callable<List<String>>) Collections::emptyList);
    }

    public static Future<String> fetchImage(String title) {
        return executorService.submit(() -> title);
    }

What about callback?

提起回调我们都会想起回调地狱,随便Google一下都能找到很多类似的代码,但回调也有它自己的优势所在,异步计算的结果完成的那一刻我们就能够收到通知,但和Future一样当有多个不同的条件时,代码就会变的很不可控,充满了各种内嵌。
下面的示例只有两层而已,试想一下如果有多个需要组合处理,那么维护这段代码将会变的非常蛋疼。

 public static void main(String[] args) {
        fetchBlogs(new Callback<List<String>>() {
            public void onComplete(List<String> data) {
                data.forEach(s -> fetchImage(s, new Callback<String>() {
                    @Override
                    public void onComplete(String data) {

                    }

                    @Override
                    public void error(Throwable throwable) {
                        throwable.printStackTrace();
                    }
                }));
            }

            public void error(Throwable throwable) {
                throwable.printStackTrace();
            }
        });
    }

    public static void fetchBlogs(Callback<List<String>> callback) {
        callback.onComplete(Collections.<String>emptyList());
    }

    public static void fetchImage(String title, Callback<String> callback) {
        callback.onComplete(title);
    }

}

interface Callback<T> {

    void onComplete(T data);

    void error(Throwable throwable);
}

Guava的解决方案

熟悉Guava的同学都知道,它提供了很多工具类从而使得我们可以更友好的编写并发代码,但是在处理多个异步任务之间的组合依赖关系时也有类似的问题, 因为本质上就是将Future的计算转换成了回调的方式提供给用户,同样会产生很多的内嵌代码。

private static final ListeningExecutorService executorService = MoreExecutors
        .listeningDecorator(Executors.newCachedThreadPool());

    public static void main(String[] args) {
        Futures.addCallback(fetchBlogs(), new FutureCallback<List<String>>() {
            @Override
            public void onSuccess(List<String> result) {
                result
                    .forEach(s -> Futures.addCallback(fetchImage(s), new FutureCallback<String>() {
                        @Override
                        public void onSuccess(String result) {

                        }

                        @Override
                        public void onFailure(Throwable t) {
                            t.printStackTrace();
                        }
                    }, executorService));
            }

            @Override
            public void onFailure(Throwable t) {
                t.printStackTrace();
            }
        }, executorService);
    }

    public static ListenableFuture<List<String>> fetchBlogs() {
        return executorService.submit(
            (Callable<List<String>>) Collections::emptyList);
    }

    public static ListenableFuture<String> fetchImage(String title) {
        return executorService.submit(() -> title);
    }

And CompletableFuture?

CompletableFuture是在JDK1.8进入的,相当于Future的升级版,其实灵感就是来自Guava的 Listenable Futures,它提供了一堆操作符让你可以将多个异步任务的处理组合成链式,从而让你可以方便处理多个future之间的依赖关系。

public static void main(String[] args) {
        fetchBlogs().thenAccept(blogs ->
            blogs.forEach(s ->
                fetchImage(s)
                    .thenAccept(s1 -> System.out.println())
                    .exceptionally(throwable -> {
                        throwable.printStackTrace();
                        return null;
                    })))
            .exceptionally(throwable -> {
                throwable.printStackTrace();
                return null;
            });
    }

    public static CompletableFuture<List<String>> fetchBlogs() {
        CompletableFuture<List<String>> completableFuture = new CompletableFuture<>();
        completableFuture.complete(Collections.emptyList());
        return completableFuture;

    }

    public static CompletableFuture<String> fetchImage(String title) {
        CompletableFuture<String> completableFuture = new CompletableFuture<>();
        completableFuture.complete("");
        return completableFuture;
    }

RxJava

Rxjava最初是由Netflix开发的Reactive programming框架,库本身非常的强大,提供了大量的操作符、更友好的错误处理、事件驱动的编程模式,屏蔽了线程安全、同步、并发数据结构等底层并发原语。
所有的方法都返回Observable,对于调用方都是一直的方法签名,而方法内部,我们想基于同步/异步、从缓存读取还是数据库读取、采用NIO还是阻塞式IO,对于调用者来说都是透明的,你不需要关心,假设我们有这个方法Data getData(),很明显目前是同步的方式,如果我们想改成异步的方式,那么就需要改动方法签名,例如返回Future或者添加一个callback参数,这就涉及了很大的改造,那么如果采用RxJava的方法,这些对于上层都是透明的。
但是RxJava的也有其问题,学习成本很高,observeOn/subscribeOn等各种新鲜的概念需要去理解,如果系统完全基于Rxjava开发,那么会调试也是个很蛋疼的问题,特别是多线程的情况下,虽然说官网以及WIKI等都提供了相当丰富的资料,但问题也在于此,太丰富了,不利于初学者上手。当然Java 9也提供了Reactive programming的解决方案,Flow API 提供了一系列接口,具体的实现可以采用Rxjava、 Vertx等。

    private static final ExecutorService executorService = Executors.newCachedThreadPool();

    public static void main(String[] args) {
        fetchBlogs()
            .subscribe(blogs ->
                    blogs.forEach(s ->
                        fetchImage(s)
                            .subscribe(System.out::println,
                                Throwable::printStackTrace)),
                Throwable::printStackTrace);
    }

    public static Observable<List<String>> fetchBlogs() {
        return Observable.create(observable -> executorService.submit(() -> {
            try {
                observable.onNext(Collections.emptyList());
                observable.onComplete();
            } catch (Exception e) {
                observable.onError(e);
            }
        }));
    }

    public static Observable<String> fetchImage(String title) {
        return Observable.create(observable -> {
            observable.onNext("");
            observable.onComplete();
        });
    }

Coroutine

Kotlin目前已经支持了Coroutine,Java也有类似的解决方案,现在也已经有了提案给Java也增加Coroutine的机制,可能很多人有一个误解,coroutine并不是说有更高的性能,而是说让我们可以像调用其他方法一样去调用并发/阻塞的方法,而调度器则向你屏蔽了底层的细节。例如一个GetXXX方法,其内部设计到了网络调用,那么我们可以中断在这里并让出CPU资源,从而使得其他coroutine可以运行,具体实现机制可以参考另一篇博客.

fun main(args: Array<String>) {
    async {
        val blogs = fetchBlogs()
        blogs.forEach {
            fetchImage(it)
        }
    }
}

suspend fun fetchBlogs(): List<String> {
    return Collections.emptyList()
}

suspend fun fetchImage(title: String): String {
    return ""
}

总结

这里简单总结了几种不同的并发编程模式,并不是说哪一种更好,而是要看看自己具体的场景,选择最合适的解决方案,万能药是不存在的,每一种药都有其用武之地、其擅长的领域。

Flag Counter

Java线程池ThreadPoolExecutor实现原理剖析

引言

在Java中,使用线程池来异步执行一些耗时任务是非常常见的操作。最初我们一般都是直接使用new Thread().start的方式,但我们知道,线程的创建和销毁都会耗费大量的资源,关于线程可以参考之前的一片博客Java线程那点事儿, 因此我们需要重用线程资源。

当然也有其他待解决方案,比如说coroutine, 目前Kotlin已经支持了,JDK也已经有了相关的提案:Project Loom, 目前的实现方式和Kotlin有点类似,都是基于ForkJoinPool,当然目前还有很多限制,以及问题没解决,比如synchronized还是锁住当前线程等。

继承结构

image
继承结构看起来很清晰,最顶层的Executor只提供了一个最简单的void execute(Runnable command)方法,然后是ExecutorService,ExecutorService提供了一些管理相关的方法,例如关闭、判断当前线程池的状态等,另外不同于Executor#execute,ExecutorService提供了一系列方法,可以将任务包装成一个Future,从而使得任务提交方可以跟踪任务的状态。而父类AbstractExecutorService则提供了一些默认的实现。

构造器

ThreadPoolExecutor的构造器提供了非常多的参数,每一个参数都非常的重要,一不小心就容易踩坑,因此设置的时候,你必须要知道自己在干什么。

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
  1. corePoolSize、 maximumPoolSize。线程池会自动根据corePoolSize和maximumPoolSize去调整当前线程池的大小。当你通过submit或者execute方法提交任务的时候,如果当前线程池的线程数小于corePoolSize,那么线程池就会创建一个新的线程处理任务, 即使其他的core线程是空闲的。如果当前线程数大于corePoolSize并且小于maximumPoolSize,那么只有在队列"满"的时候才会创建新的线程。因此这里会有很多的坑,比如你的core和max线程数设置的不一样,希望请求积压在队列的时候能够实时的扩容,但如果制定了一个无界队列,那么就不会扩容了,因为队列不存在满的概念。

  2. keepAliveTime。如果当前线程池中的线程数超过了corePoolSize,那么如果在keepAliveTime时间内都没有新的任务需要处理,那么超过corePoolSize的这部分线程就会被销毁。默认情况下是不会回收core线程的,可以通过设置allowCoreThreadTimeOut改变这一行为。

  3. workQueue。即实际用于存储任务的队列,这个可以说是最核心的一个参数了,直接决定了线程池的行为,比如说传入一个有界队列,那么队列满的时候,线程池就会根据core和max参数的设置情况决定是否需要扩容,如果传入了一个SynchronousQueue,这个队列只有在另一个线程在同步remove的时候才可以put成功,对应到线程池中,简单来说就是如果有线程池任务处理完了,调用poll或者take方法获取新的任务的时候,新提交的任务才会put成功,否则如果当前的线程都在忙着处理任务,那么就会put失败,也就会走扩容的逻辑,如果传入了一个DelayedWorkQueue,顾名思义,任务就会根据过期时间来决定什么时候弹出,即为ScheduledThreadPoolExecutor的机制。

  4. threadFactory。创建线程都是通过ThreadFactory来实现的,如果没指定的话,默认会使用Executors.defaultThreadFactory() ,一般来说,我们会在这里对线程设置名称、异常处理器等。

  5. handler。即当任务提交失败的时候,会调用这个处理器,ThreadPoolExecutor内置了多个实现,比如抛异常、直接抛弃等。这里也需要根据业务场景进行设置,比如说当队列积压的时候,针对性的对线程池扩容或者发送告警等策略。

看完这几个参数的含义,我们看一下Executors提供的一些工具方法,只要是为了方便使用,但是我建议最好少用这个类,而是直接用ThreadPoolExecutor的构造函数,多了解一下这几个参数到底是什么意思,自己的业务场景是什么样的,比如线程池需不需要扩容、用不用回收空闲的线程等。

public class Executors {
    
    /*
    * 提供一个固定大小的线程池,并且线程不会回收,由于传入的是一个无界队列,相当于队列永远不会满
    * 也就不会扩容,因此需要特别注意任务积压在队列中导致内存爆掉的问题
    */
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }


    /*
    *  这个线程池会一直扩容,由于SynchronousQueue的特性,如果当前所有的线程都在处理任务,那么
    *  新的请求过来,就会导致创建一个新的线程处理任务。如果线程一分钟没有新任务处理,就会被回 
    *  收掉。特别注意,如果每一个任务都比较耗时,并发又比较高,那么可能每次任务过来都会创建一个线 
    *  程
    */
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
}

源码分析

既然是个线程池,那就必然有其生命周期:运行中、关闭、停止等。ThreadPoolExecutor是用一个AtomicInteger去的前三位表示这个状态的,另外又重用了低29位用于表示线程数,可以支持最大大概5亿多,绝逼够用了,如果以后硬件真的发展到能够启动这么多线程,改成AtomicLong就可以了。
状态这里主要分为下面几种:

  1. RUNNING: 表示当前线程池正在运行中,可以接受新任务以及处理队列中的任务
  2. SHUTDOWN: 不再接受新的任务,但会继续处理队列中的任务
  3. STOP: 不再接受新的任务,也不处理队列中的任务了,并且会中断正在进行中的任务
  4. TIDYING: 所有任务都已经处理完毕,线程数为0,转为为TIDYING状态之后,会调用terminated()回调
  5. TERMINATED: terminated()已经执行完毕

同时我们可以看到所有的状态都是用二进制位表示的,并且依次递增,从而方便进行比较,比如想获取当前状态是否至少为SHUTDOWN等,同时状态之前有几种转换:

  1. RUNNING -> SHUTDOWN。调用了shutdown()之后,或者执行了finalize()
  2. (RUNNING 或者 SHUTDOWN) -> STOP。调用了shutdownNow()之后会转换这个状态
  3. SHUTDOWN -> TIDYING。当线程池和队列都为空的时候
  4. STOP -> TIDYING。当线程池为空的时候
  5. IDYING -> TERMINATED。执行完terminated()回调之后会转换为这个状态
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    private static final int COUNT_BITS = Integer.SIZE - 3;
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

    private static final int RUNNING    = -1 << COUNT_BITS;
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    private static final int STOP       =  1 << COUNT_BITS;
    private static final int TIDYING    =  2 << COUNT_BITS;
    private static final int TERMINATED =  3 << COUNT_BITS;

    //由于前三位表示状态,因此将CAPACITY取反,和进行与操作即可
    private static int runStateOf(int c)     { return c & ~CAPACITY; }
    private static int workerCountOf(int c)  { return c & CAPACITY; }
    
    //高三位+第三位进行或操作即可
    private static int ctlOf(int rs, int wc) { return rs | wc; }

    private static boolean runStateLessThan(int c, int s) {
        return c < s;
    }

    private static boolean runStateAtLeast(int c, int s) {
        return c >= s;
    }

    private static boolean isRunning(int c) {
        return c < SHUTDOWN;
    }
    
    //下面三个方法,通过CAS修改worker的数目
    private boolean compareAndIncrementWorkerCount(int expect) {
        return ctl.compareAndSet(expect, expect + 1);
    }
    
    //只尝试一次,失败了则返回,是否重试由调用方决定
    private boolean compareAndDecrementWorkerCount(int expect) {
        return ctl.compareAndSet(expect, expect - 1);
    }
    
    //跟上一个不一样,会一直重试
    private void decrementWorkerCount() {
        do {} while (! compareAndDecrementWorkerCount(ctl.get()));
    }

下面是比较核心的字段,这里workers采用的是非线程安全的HashSet, 而不是线程安全的版本,主要是因为这里有些复合的操作,比如说将worker添加到workers后,我们还需要判断是否需要更新largestPoolSize等,workers只在获取到mainLock的情况下才会进行读写,另外这里的mainLock也用于在中断线程的时候串行执行,否则如果不加锁的话,可能会造成并发去中断线程,引起不必要的中断风暴。

private final ReentrantLock mainLock = new ReentrantLock();

private final HashSet<Worker> workers = new HashSet<Worker>();

private final Condition termination = mainLock.newCondition();

private int largestPoolSize;

private long completedTaskCount;

核心方法

拿到一个线程池之后,我们就可以开始提交任务,让它去执行了,那么我们看一下submit方法是如何实现的。

    public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }

    public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        execute(ftask);
        return ftask;
    }

这两个方法都很简单,首先将提交过来的任务(有两种形式:Callable、Runnable )都包装成统一的 RunnableFuture,然后调用execute方法,execute可以说是线程池最核心的一个方法。

    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        /*
            获取当前worker的数目,如果小于corePoolSize那么就扩容,
            这里不会判断是否已经有core线程,而是只要小于corePoolSize就会直接增加worker
         */
        if (workerCountOf(c) < corePoolSize) {
            /*
                调用addWorker(Runnable firstTask, boolean core)方法扩容
                firstTask表示为该worker启动之后要执行的第一个任务,core表示要增加的为core线程
             */
            if (addWorker(command, true))
                return;
            //如果增加失败了那么重新获取ctl的快照,比如可能线程池在这期间关闭了
            c = ctl.get();
        }
        /*
             如果当前线程池正在运行中,并且将任务丢到队列中成功了,
             那么就会进行一次double check,看下在这期间线程池是否关闭了,
             如果关闭了,比如处于SHUTDOWN状态,如上文所讲的,SHUTDOWN状态的时候,
             不再接受新任务,remove成功后调用拒绝处理器。而如果仍然处于运行中的状态,
             那么这里就double check下当前的worker数,如果为0,有可能在上述逻辑的执行
             过程中,有worker销毁了,比如说任务抛出了未捕获异常等,那么就会进行一次扩容,
             但不同于扩容core线程,这里由于任务已经丢到队列中去了,因此就不需要再传递firstTask了,
             同时要注意,这里扩容的是非core线程
         */
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            /*
                如果在上一步中,将任务丢到队列中失败了,那么就进行一次扩容,
                这里会将任务传递到firstTask参数中,并且扩容的是非core线程,
                如果扩容失败了,那么就执行拒绝策略。
             */
            reject(command);
    }

这里要特别注意下防止队列失败的逻辑,不同的队列丢任务的逻辑也不一样,例如说无界队列,那么就永远不会put失败,也就是说扩容也永远不会执行,如果是有界队列,那么当队列满的时候,会扩容非core线程,如果是SynchronousQueue,这个队列比较特殊,当有另外一个线程正在同步获取任务的时候,你才能put成功,因此如果当前线程池中所有的worker都忙着处理任务的时候,那么后续的每次新任务都会导致扩容, 当然如果worker没有任务处理了,阻塞在获取任务这一步的时候,新任务的提交就会直接丢到队列中去,而不会扩容。
上文中多次提到了扩容,那么我们下面看一下线程池具体是如何进行扩容的:

    private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (;;) {
            int c = ctl.get();
            //获取当前线程池的状态
            int rs = runStateOf(c);

            /*
                如果状态为大于SHUTDOWN, 比如说STOP,STOP上文说过队列中的任务不处理了,也不接受新任务,
                因此可以直接返回false不扩容了,如果状态为SHUTDOWN并且firstTask为null,同时队列非空,
                那么就可以扩容
             */
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                    firstTask == null &&
                    ! workQueue.isEmpty()))
                return false;

            for (;;) {
                int wc = workerCountOf(c);
                /*
                    若worker的数目大于CAPACITY则直接返回,
                    然后根据要扩容的是core线程还是非core线程,进行判断worker数目
                    是否超过设置的值,超过则返回
                 */
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                /*
                    通过CAS的方式自增worker的数目,成功了则直接跳出循环
                 */
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                //重新读取状态变量,如果状态改变了,比如线程池关闭了,那么就跳到最外层的for循环,
                //注意这里跳出的是retry。
                c = ctl.get();  // Re-read ctl
                if (runStateOf(c) != rs)
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }

        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            //创建Worker
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    /*
                        获取锁,并判断线程池是否已经关闭
                     */
                    int rs = runStateOf(ctl.get());

                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive()) // 若线程已经启动了,比如说已经调用了start()方法,那么就抛异常,
                            throw new IllegalThreadStateException();
                        //添加到workers中
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize) //更新largestPoolSize
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    //若Worker创建成功,则启动线程,这么时候worker就会开始执行任务了
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                //添加失败
                addWorkerFailed(w);
        }
        return workerStarted;
    } 

    private void addWorkerFailed(Worker w) {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            if (w != null)
                workers.remove(w);
            decrementWorkerCount();
            //每次减少worker或者从队列中移除任务的时候都需要调用这个方法
            tryTerminate();
        } finally {
            mainLock.unlock();
        }
    }

这里有个貌似不太起眼的方法tryTerminate,这个方法会在所有可能导致线程池终结的地方调用,比如说减少worker的数目等,如果满足条件的话,那么将线程池转换为TERMINATED状态。另外这个方法没有用private修饰,因为ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,而ScheduledThreadPoolExecutor也会调用这个方法。

    final void tryTerminate() {
        for (;;) {
            int c = ctl.get();
            /*
                如果当前线程处于运行中、TIDYING、TERMINATED状态则直接返回,运行中的没
                什么好说的,后面两种状态可以说线程池已经正在终结了,另外如果处于SHUTDOWN状态,
                并且workQueue非空,表明还有任务需要处理,也直接返回
             */
            if (isRunning(c) ||
                runStateAtLeast(c, TIDYING) ||
                (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
                return;
            //可以退出,但是线程数非0,那么就中断一个线程,从而使得关闭的信号能够传递下去,
            //中断worker后,worker捕获异常后,会尝试退出,并在这里继续执行tryTerminate()方法,
            //从而使得信号传递下去
            if (workerCountOf(c) != 0) {
                interruptIdleWorkers(ONLY_ONE);
                return;
            }

            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                //尝试转换成TIDYING状态,执行完terminated回调之后
                //会转换为TERMINATED状态,这个时候线程池已经完整关闭了,
                //通过signalAll方法,唤醒所有阻塞在awaitTermination上的线程
                if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
                    try {
                        terminated();
                    } finally {
                        ctl.set(ctlOf(TERMINATED, 0));
                        termination.signalAll();
                    }
                    return;
                }
            } finally {
                mainLock.unlock();
            }
            // else retry on failed CAS
        }
    }

    /**
     * 中断空闲的线程
     * @param onlyOne
     */
    private void interruptIdleWorkers(boolean onlyOne) {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            for (Worker w : workers) {
                //遍历所有worker,若之前没有被中断过,
                //并且获取锁成功,那么就尝试中断。
                //锁能够获取成功,那么表明当前worker没有在执行任务,而是在
                //获取任务,因此也就达到了只中断空闲线程的目的。
                Thread t = w.thread;
                if (!t.isInterrupted() && w.tryLock()) {
                    try {
                        t.interrupt();
                    } catch (SecurityException ignore) {
                    } finally {
                        w.unlock();
                    }
                }
                if (onlyOne)
                    break;
            }
        } finally {
            mainLock.unlock();
        }
    }

image

Worker

下面看一下Worker类,也就是这个类实际负责执行任务,Worker类继承自AbstractQueuedSynchronizer,AQS可以理解为一个同步框架,提供了一些通用的机制,利用模板方法模式,让你能够原子的管理同步状态、blocking和unblocking线程、以及队列,具体的内容之后有时间会再写,还是比较复杂的。这里Worker对AQS的使用相对比较简单,使用了状态变量state表示是否获得锁,0表示解锁、1表示已获得锁,同时通过exclusiveOwnerThread存储当前持有锁的线程。另外再简单提一下,比如说CountDownLatch, 也是基于AQS框架实现的,countdown方法递减state,await阻塞等待state为0。

private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
    {
      
        /** Thread this worker is running in.  Null if factory fails. */
        final Thread thread;

        /** Initial task to run.  Possibly null. */
        Runnable firstTask;

        /** Per-thread task counter */
        volatile long completedTasks;

        Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }

        /** Delegates main run loop to outer runWorker  */
        public void run() {
            runWorker(this);
        }
       protected boolean isHeldExclusively() {
            return getState() != 0;
        }

        protected boolean tryAcquire(int unused) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        protected boolean tryRelease(int unused) {
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        public void lock()        { acquire(1); }
        public boolean tryLock()  { return tryAcquire(1); }
        public void unlock()      { release(1); }
        public boolean isLocked() { return isHeldExclusively(); }

        void interruptIfStarted() {
            Thread t;
            if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                }
            }
        }
    }

注意这里Worker初始化的时候,会通过setState(-1)将state设置为-1,并在runWorker()方法中置为0,上文说过Worker是利用state这个变量来表示锁的状态,那么加锁的操作就是通过CAS将state从0改成1,那么初始化的时候改成-1,也就是表示在Worker启动之前,都不允许加锁操作,我们再看interruptIfStarted()以及interruptIdleWorkers()方法,这两个方法在尝试中断Worker之前,都会先加锁或者判断state是否大于0,因此这里的将state设置为-1,就是为了禁止中断操作,并在runWorker中置为0,也就是说只能在Worker启动之后才能够中断Worker。
另外线程启动之后,其实就是调用了runWorker方法,下面我们看一下具体是如何实现的。

   final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // 调用unlock()方法,将state置为0,表示其他操作可以获得锁或者中断worker
        boolean completedAbruptly = true;
        try {
            /*
                首先尝试执行firstTask,若没有的话,则调用getTask()从队列中获取任务
             */
            while (task != null || (task = getTask()) != null) {
                w.lock();
                /*
                    如果线程池正在关闭,那么中断线程。
                 */
                if ((runStateAtLeast(ctl.get(), STOP) ||
                    (Thread.interrupted() &&
                        runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    //执行beforeExecute回调
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        //实际开始执行任务
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                        //执行afterExecute回调
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    //这里加了锁,因此没有线程安全的问题,volatile修饰保证其他线程的可见性
                    w.completedTasks++;
                    w.unlock();//解锁
                }
            }
            completedAbruptly = false;
        } finally {
            //抛异常了,或者当前队列中已没有任务需要处理等
            processWorkerExit(w, completedAbruptly);
        }
    }

private void processWorkerExit(Worker w, boolean completedAbruptly) {
        //如果是异常终止的,那么减少worker的数目
        if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
            decrementWorkerCount();

        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            //将当前worker中workers中删除掉,并累加当前worker已执行的任务到completedTaskCount中
            completedTaskCount += w.completedTasks;
            workers.remove(w);
        } finally {
            mainLock.unlock();
        }

        //上文说过,减少worker的操作都需要调用这个方法
        tryTerminate();

        /*
            如果当前线程池仍然是运行中的状态,那么就看一下是否需要新增另外一个worker替换此worker
         */
        int c = ctl.get();
        if (runStateLessThan(c, STOP)) {
            /*
                如果是异常结束的则直接扩容,否则的话则为正常退出,比如当前队列中已经没有任务需要处理,
                如果允许core线程超时的话,那么看一下当前队列是否为空,空的话则不用扩容。否则话看一下
                是否少于corePoolSize个worker在运行。
             */
            if (!completedAbruptly) {
                int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
                if (min == 0 && ! workQueue.isEmpty())
                    min = 1;
                if (workerCountOf(c) >= min)
                    return; // replacement not needed
            }
            addWorker(null, false);
        }
    }

     private Runnable getTask() {
        boolean timedOut = false; // 上一次poll()是否超时了

        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // 若线程池关闭了(状态大于STOP)
            // 或者线程池处于SHUTDOWN状态,但是队列为空,那么返回null
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }

            int wc = workerCountOf(c);

            /*
                如果允许core线程超时 或者 不允许core线程超时但当前worker的数目大于core线程数,
                那么下面的poll()则超时调用
             */
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

            /*
                获取任务超时了并且(当前线程池中还有不止一个worker 或者 队列中已经没有任务了),那么就尝试
                减少worker的数目,若失败了则重试
             */
            if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;
            }

            try {
                //从队列中抓取任务
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                //走到这里表明,poll调用超时了
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }

关闭线程池

关闭线程池一般有两种形式,shutdown()和shutdownNow()

    public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            //通过CAS将状态更改为SHUTDOWN,这个时候线程池不接受新任务,但会继续处理队列中的任务
            advanceRunState(SHUTDOWN);
            //中断所有空闲的worker,也就是说除了正在处理任务的worker,其他阻塞在getTask()上的worker
            //都会被中断
            interruptIdleWorkers();
            //执行回调
            onShutdown(); // hook for ScheduledThreadPoolExecutor
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
        //这个方法不会等待所有的任务处理完成才返回
    }
    public List<Runnable> shutdownNow() {
        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            /*
                不同于shutdown(),会转换为STOP状态,不再处理新任务,队列中的任务也不处理,
                而且会中断所有的worker,而不只是空闲的worker
             */
            advanceRunState(STOP);
            interruptWorkers();
            tasks = drainQueue();//将所有的任务从队列中弹出
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
        return tasks;
    }

    private List<Runnable> drainQueue() {
        BlockingQueue<Runnable> q = workQueue;
        ArrayList<Runnable> taskList = new ArrayList<Runnable>();
        /*
            将队列中所有的任务remove掉,并添加到taskList中,
            但是有些队列比较特殊,比如说DelayQueue,如果第一个任务还没到过期时间,则不会弹出,
            因此这里通过调用toArray方法,然后再一个一个的remove掉
         */
        q.drainTo(taskList);
        if (!q.isEmpty()) {
            for (Runnable r : q.toArray(new Runnable[0])) {
                if (q.remove(r))
                    taskList.add(r);
            }
        }
        return taskList;
    }

从上文中可以看到,调用了shutdown()方法后,不会等待所有的任务处理完毕才返回,因此需要调用awaitTermination()来实现

    public boolean awaitTermination(long timeout, TimeUnit unit)
        throws InterruptedException {
        long nanos = unit.toNanos(timeout);
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            for (;;) {
                //线程池若已经终结了,那么就返回
                if (runStateAtLeast(ctl.get(), TERMINATED))
                    return true;
                //若超时了,也返回掉
                if (nanos <= 0)
                    return false;
                //阻塞在信号量上,等待线程池终结,但是要注意这个方法可能会因为一些未知原因随时唤醒当前线程,
                //因此需要重试,在tryTerminate()方法中,执行完terminated()回调后,表明线程池已经终结了,
                //然后会通过termination.signalAll()唤醒当前线程
                nanos = termination.awaitNanos(nanos);
            }
        } finally {
            mainLock.unlock();
        }
    }

一些统计相关的方法

    public int getPoolSize() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            //若线程已终结则直接返回0,否则计算works中的数目
           //想一下为什么不用workerCount呢?
            return runStateAtLeast(ctl.get(), TIDYING) ? 0
                : workers.size();
        } finally {
            mainLock.unlock();
        }
    }

   public int getActiveCount() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            int n = 0;
            for (Worker w : workers)
                if (w.isLocked())//上锁的表明worker当前正在处理任务,也就是活跃的worker
                    ++n;
            return n;
        } finally {
            mainLock.unlock();
        }
    }


    public int getLargestPoolSize() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            return largestPoolSize;
        } finally {
            mainLock.unlock();
        }
    }

    //获取任务的总数,这个方法慎用,若是个无解队列,或者队列挤压比较严重,会很蛋疼
    public long getTaskCount() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            long n = completedTaskCount;//比如有些worker被销毁后,其处理完成的任务就会叠加到这里
            for (Worker w : workers) {
                n += w.completedTasks;//叠加历史处理完成的任务
                if (w.isLocked())//上锁表明正在处理任务,也算一个
                    ++n;
            }
            return n + workQueue.size();//获取队列中的数目
        } finally {
            mainLock.unlock();
        }
    }


    public long getCompletedTaskCount() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            long n = completedTaskCount;
            for (Worker w : workers)
                n += w.completedTasks;
            return n;
        } finally {
            mainLock.unlock();
        }
    }

总结

这篇博客基本上覆盖了线程池的方方面面,但仍然有非常多的细节可以深究,比如说异常的处理,可以参照之前的一篇博客:深度解析Java线程池的异常处理机制 ,另外还有AQS、unsafe等可以之后再单独总结。

Flag Counter

MongoDB导出场景查询优化

引言

前段时间遇到一个类似导出数据场景,观察下来发现速度会越来越慢,导出100万数据需要耗费40-60分钟,从日志观察发现,耗时也是越来越高。

原因

从代码逻辑上看,这里采取了分批次导出的方式,类似前端的分页,具体是通过skip+limit的方式实现的,那么采用这种方式会有什么问题呢?我们google一下这两个接口的文档:

The cursor.skip() method is often expensive because it requires the server to walk from the 
beginning of the collection or index to get the offset or skip position before beginning to return 
results. As the offset (e.g. pageNumber above) increases, cursor.skip() will become slower and 
more CPU intensive. With larger collections, cursor.skip() may become IO bound.

简单来说,随着页数的增长,skip()会变得越来越慢,但是具体就我们这里导出的场景来说,按理说应该没必要每次都去重复计算,做一些无用功,我的理解应该可以拿到一个指针,慢慢遍历,简单google之后,我们发现果然是可以这样做的。

我们可以在持久层新增一个方法,返回一个cursor专门供上层去遍历数据,这样就不用再去遍历已经导出过的结果集,从O(N2)优化到了O(N),这里还可以指定一个batchSize,设置一次从MongoDB中抓取的数据量(元素个数),注意这里最大是4M.

/**
     * <p>Limits the number of elements returned in one batch. A cursor 
     * typically fetches a batch of result objects and store them
     * locally.</p>
     *
     * <p>If {@code batchSize} is positive, it represents the size of each batch of objects retrieved. It can be adjusted to optimize
     * performance and limit data transfer.</p>
     *
     * <p>If {@code batchSize} is negative, it will limit of number objects returned, that fit within the max batch size limit (usually
     * 4MB), and cursor will be closed. For example if {@code batchSize} is -10, then the server will return a maximum of 10 documents and
     * as many as can fit in 4MB, then close the cursor. Note that this feature is different from limit() in that documents must fit within
     * a maximum size, and it removes the need to send a request to close the cursor server-side.</p>
*/

比如说我这里配置的8000,那么mongo客户端就会去默认抓取这么多的数据量:

image

经过本地简单的测试,我们发现性能已经有了飞跃的提升,导出30万数据,采用之前的方式,翻页到后面平均要500ms,总耗时60039ms。而优化后的方式,平均耗时在100ms-200ms之间,总耗时16667ms(中间包括业务逻辑的耗时)。

使用

DBCursor cursor = collection.find(query).batchSize(8000);
while (dbCursor.hasNext()) {
  DBObject nextItem = dbCursor.next();
  //业务代码
  ... 
  //
}

那么我们再看看hasNext内部的逻辑好吗?好的.

    @Override
    public boolean hasNext() {
        if (closed) {
            throw new IllegalStateException("Cursor has been closed");
        }

        if (nextBatch != null) {
            return true;
        }

        if (limitReached()) {
            return false;
        }

        while (serverCursor != null) {
            //这里会向mongo发送一条指令去抓取数据
            getMore();
            if (nextBatch != null) {
                return true;
            }
        }

        return false;
    }
    
    
    private void getMore() {
        Connection connection = connectionSource.getConnection();
        try {
            if(serverIsAtLeastVersionThreeDotTwo(connection.getDescription()){
                try {
//可以看到这里其实是调用了`nextBatch`指令        
initFromCommandResult(connection.command(namespace.getDatabaseName(),
                                                             asGetMoreCommandDocument(),
                                                             false,
                                                             new NoOpFieldNameValidator(),
                                                             CommandResultDocumentCodec.create(decoder, "nextBatch")));
                } catch (MongoCommandException e) {
                    throw translateCommandException(e, serverCursor);
                }
            } else {
                initFromQueryResult(connection.getMore(namespace, serverCursor.getId(),
                                                       getNumberToReturn(limit, batchSize, count),
                                                       decoder));
            }
            if (limitReached()) {
                killCursor(connection);
            }
        } finally {
            connection.release();
        }
    }

最后initFromCommandResult 拿到结果并解析成Bson对象

总结

我们平常写代码的时候,最好都能够针对每个方法、接口甚至是更细的粒度加上埋点,也可以设置成debug级别,这样利用log4j/logback等日志框架动态更新级别,可以随时查看耗时,从而更能够针对性的优化,对于本文说的这个场景,我们首先看看是不是代码的逻辑有问题,然后看看是不是数据库的问题,比如说没建索引、数据量过大等,再去想办法针对性的优化,而不要上来就撸代码。

Flag Counter

Spring Cloud Ribbon踩坑记录及原理解析

声明:代码不是我写的=_=

现象

前两天碰到一个ribbon相关的问题,觉得值得简单记录一下。表象是对外的接口返回内部异常,这个是封装的统一错误信息,Spring的异常处理器catch到未捕获异常统一返回的信息。因此到日志平台查看实际的异常:

org.springframework.web.client.HttpClientErrorException: 404 null

这里介绍一下背景,出现问题的开放网关,做点事情说白了就是转发对应的请求给后端的服务。这里用到了ribbon去做服务负载均衡、eureka负责服务发现。
这里出现404,首先看了下请求的url以及对应的参数,都没有发现问题,对应的后端服务也没有收到请求。这就比较诡异了,开始怀疑是ribbon或者Eureka的缓存导致请求到了错误的ip或端口,但由于日志中打印的是Eureka的serviceId而不是实际的ip:port,因此先加了个日志:

@Slf4j
public class CustomHttpRequestInterceptor implements ClientHttpRequestInterceptor {

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        log.info("Request , url:{},method:{}.", request.getURI(), request.getMethod());
        return execution.execute(request, body);
    }
}

这里是通过给RestTemplate添加拦截器的方式,但要注意,ribbon也是通过给RestTemplate添加拦截器实现的解析serviceId到实际的ip:port,因此需要注意下优先级添加到ribbon的LoadBalancerInterceptor之后,我这里是通过Spring的初始化完成事件的回调中添加的,另外也添加了另一条日志,在catch到这个异常的时候,利用Eureka的DiscoveryClient#getInstances获取到当前的实例信息。
之后在测试环境中复现了这个问题,看了下日志,eurek中缓存的实例信息是对的,但是实际调用的确实另外一个服务的地址,从而导致了接口404。

源码解析

从上述的信息中可以知道,问题出在ribbon中,具体的原因后面会说,这里先讲一下Spring Cloud Ribbon的初始化流程。

@Configuration
@ConditionalOnClass({ IClient.class, RestTemplate.class, AsyncRestTemplate.class, Ribbon.class})
@RibbonClients
@AutoConfigureAfter(name = "org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration")
@AutoConfigureBefore({LoadBalancerAutoConfiguration.class, AsyncLoadBalancerAutoConfiguration.class})
@EnableConfigurationProperties({RibbonEagerLoadProperties.class, ServerIntrospectorProperties.class})
public class RibbonAutoConfiguration {
}

注意这个注解@RibbonClients, 如果想要覆盖Spring Cloud提供的默认Ribbon配置就可以使用这个注解,最终的解析类是:

public class RibbonClientConfigurationRegistrar implements ImportBeanDefinitionRegistrar {

	@Override
	public void registerBeanDefinitions(AnnotationMetadata metadata,
			BeanDefinitionRegistry registry) {
		Map<String, Object> attrs = metadata.getAnnotationAttributes(
				RibbonClients.class.getName(), true);
		if (attrs != null && attrs.containsKey("value")) {
			AnnotationAttributes[] clients = (AnnotationAttributes[]) attrs.get("value");
			for (AnnotationAttributes client : clients) {
				registerClientConfiguration(registry, getClientName(client),
						client.get("configuration"));
			}
		}
		if (attrs != null && attrs.containsKey("defaultConfiguration")) {
			String name;
			if (metadata.hasEnclosingClass()) {
				name = "default." + metadata.getEnclosingClassName();
			} else {
				name = "default." + metadata.getClassName();
			}
			registerClientConfiguration(registry, name,
					attrs.get("defaultConfiguration"));
		}
		Map<String, Object> client = metadata.getAnnotationAttributes(
				RibbonClient.class.getName(), true);
		String name = getClientName(client);
		if (name != null) {
			registerClientConfiguration(registry, name, client.get("configuration"));
		}
	}

	private String getClientName(Map<String, Object> client) {
		if (client == null) {
			return null;
		}
		String value = (String) client.get("value");
		if (!StringUtils.hasText(value)) {
			value = (String) client.get("name");
		}
		if (StringUtils.hasText(value)) {
			return value;
		}
		throw new IllegalStateException(
				"Either 'name' or 'value' must be provided in @RibbonClient");
	}

	private void registerClientConfiguration(BeanDefinitionRegistry registry,
			Object name, Object configuration) {
		BeanDefinitionBuilder builder = BeanDefinitionBuilder
				.genericBeanDefinition(RibbonClientSpecification.class);
		builder.addConstructorArgValue(name);
		builder.addConstructorArgValue(configuration);
		registry.registerBeanDefinition(name + ".RibbonClientSpecification",
				builder.getBeanDefinition());
	}
}

atrrs包含defaultConfiguration,因此会注册RibbonClientSpecification类型的bean,注意名称以default.开头,类型是RibbonAutoConfiguration,注意上面说的RibbonAutoConfiguration被@RibbonClients修饰。
然后再回到上面的源码:

public class RibbonAutoConfiguration {
       

        //上文中会解析被@RibbonClients注解修饰的类,然后注册类型为RibbonClientSpecification的bean。
        //主要有两个: RibbonAutoConfiguration、RibbonEurekaAutoConfiguration
	@Autowired(required = false)
	private List<RibbonClientSpecification> configurations = new ArrayList<>();
      
	@Bean
	public SpringClientFactory springClientFactory() {
                //初始化SpringClientFactory,并将上面的配置注入进去,这段很重要。
		SpringClientFactory factory = new SpringClientFactory();
		factory.setConfigurations(this.configurations);
		return factory;
	}
         //其他的都是提供一些默认的bean配置

	@Bean
	@ConditionalOnMissingBean(LoadBalancerClient.class)
	public LoadBalancerClient loadBalancerClient() {
		return new RibbonLoadBalancerClient(springClientFactory());
	}

	@Bean
	@ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate")
	@ConditionalOnMissingBean
	public LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory(SpringClientFactory clientFactory) {
		return new RibbonLoadBalancedRetryPolicyFactory(clientFactory);
	}

	@Bean
	@ConditionalOnMissingClass(value = "org.springframework.retry.support.RetryTemplate")
	@ConditionalOnMissingBean
	public LoadBalancedRetryPolicyFactory neverRetryPolicyFactory() {
		return new LoadBalancedRetryPolicyFactory.NeverRetryFactory();
	}

	@Bean
	@ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate")
	@ConditionalOnMissingBean
	public LoadBalancedBackOffPolicyFactory loadBalancedBackoffPolicyFactory() {
		return new LoadBalancedBackOffPolicyFactory.NoBackOffPolicyFactory();
	}

	@Bean
	@ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate")
	@ConditionalOnMissingBean
	public LoadBalancedRetryListenerFactory loadBalancedRetryListenerFactory() {
		return new LoadBalancedRetryListenerFactory.DefaultRetryListenerFactory();
	}

	@Bean
	@ConditionalOnMissingBean
	public PropertiesFactory propertiesFactory() {
		return new PropertiesFactory();
	}
	
	@Bean
	@ConditionalOnProperty(value = "ribbon.eager-load.enabled", matchIfMissing = false)
	public RibbonApplicationContextInitializer ribbonApplicationContextInitializer() {
		return new RibbonApplicationContextInitializer(springClientFactory(),
				ribbonEagerLoadProperties.getClients());
	}

	@Configuration
	@ConditionalOnClass(HttpRequest.class)
	@ConditionalOnRibbonRestClient
	protected static class RibbonClientConfig {

		@Autowired
		private SpringClientFactory springClientFactory;

		@Bean
		public RestTemplateCustomizer restTemplateCustomizer(
				final RibbonClientHttpRequestFactory ribbonClientHttpRequestFactory) {
			return new RestTemplateCustomizer() {
				@Override
				public void customize(RestTemplate restTemplate) {
					restTemplate.setRequestFactory(ribbonClientHttpRequestFactory);
				}
			};
		}

		@Bean
		public RibbonClientHttpRequestFactory ribbonClientHttpRequestFactory() {
			return new RibbonClientHttpRequestFactory(this.springClientFactory);
		}
	}

	//TODO: support for autoconfiguring restemplate to use apache http client or okhttp

	@Target({ ElementType.TYPE, ElementType.METHOD })
	@Retention(RetentionPolicy.RUNTIME)
	@Documented
	@Conditional(OnRibbonRestClientCondition.class)
	@interface ConditionalOnRibbonRestClient { }

	private static class OnRibbonRestClientCondition extends AnyNestedCondition {
		public OnRibbonRestClientCondition() {
			super(ConfigurationPhase.REGISTER_BEAN);
		}

		@Deprecated //remove in Edgware"
		@ConditionalOnProperty("ribbon.http.client.enabled")
		static class ZuulProperty {}

		@ConditionalOnProperty("ribbon.restclient.enabled")
		static class RibbonProperty {}
	}
}

注意这里的SpringClientFactory, ribbon默认情况下,每个eureka的serviceId(服务),都会分配自己独立的Spring的上下文,即ApplicationContext, 然后这个上下文中包含了必要的一些bean,比如: ILoadBalancer ServerListFilter 等。而Spring Cloud默认是使用RestTemplate封装了ribbon的调用,核心是通过一个拦截器:

@Bean
		@ConditionalOnMissingBean
		public RestTemplateCustomizer restTemplateCustomizer(
				final LoadBalancerInterceptor loadBalancerInterceptor) {
			return new RestTemplateCustomizer() {
				@Override
				public void customize(RestTemplate restTemplate) {
					List<ClientHttpRequestInterceptor> list = new ArrayList<>(
							restTemplate.getInterceptors());
					list.add(loadBalancerInterceptor);
					restTemplate.setInterceptors(list);
				}
			};
		}

因此核心是通过这个拦截器实现的负载均衡:

public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {

	private LoadBalancerClient loadBalancer;
	private LoadBalancerRequestFactory requestFactory;

	@Override
	public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
			final ClientHttpRequestExecution execution) throws IOException {
		final URI originalUri = request.getURI(); //这里传入的url是解析之前的,即http://serviceId/服务地址的形式
		String serviceName = originalUri.getHost(); //解析拿到对应的serviceId
		Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
		return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
	}
}

然后将请求转发给LoadBalancerClient:

public class RibbonLoadBalancerClient implements LoadBalancerClient {
@Override
	public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
		ILoadBalancer loadBalancer = getLoadBalancer(serviceId); //获取对应的LoadBalancer
		Server server = getServer(loadBalancer); //获取服务器,这里会执行对应的分流策略,比如轮训
                //、随机等
		if (server == null) {
			throw new IllegalStateException("No instances available for " + serviceId);
		}
		RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server,
				serviceId), serverIntrospector(serviceId).getMetadata(server));

		return execute(serviceId, ribbonServer, request);
	}
}

而这里的LoadBalancer是通过上文中提到的SpringClientFactory获取到的,这里会初始化一个新的Spring上下文,然后将Ribbon默认的配置类,比如说:RibbonAutoConfigurationRibbonEurekaAutoConfiguration等添加进去, 然后将当前spring的上下文设置为parent,再调用refresh方法进行初始化。

public class SpringClientFactory extends NamedContextFactory<RibbonClientSpecification> {
	protected AnnotationConfigApplicationContext createContext(String name) {
		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
		if (this.configurations.containsKey(name)) {
			for (Class<?> configuration : this.configurations.get(name)
					.getConfiguration()) {
				context.register(configuration);
			}
		}
		for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
			if (entry.getKey().startsWith("default.")) {
				for (Class<?> configuration : entry.getValue().getConfiguration()) {
					context.register(configuration);
				}
			}
		}
		context.register(PropertyPlaceholderAutoConfiguration.class,
				this.defaultConfigType);
		context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(
				this.propertySourceName,
				Collections.<String, Object> singletonMap(this.propertyName, name)));
		if (this.parent != null) {
			// Uses Environment from parent as well as beans
			context.setParent(this.parent);
		}
		context.refresh();
		return context;
	}
}

最核心的就在这一段,也就是说对于每一个不同的serviceId来说,都拥有一个独立的spring上下文,并且在第一次调用这个服务的时候,会初始化ribbon相关的所有bean, 如果不存在 才回去父context中去找。

再回到上文中根据分流策略获取实际的ip:port的代码段:

public class RibbonLoadBalancerClient implements LoadBalancerClient {
@Override
	public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
		ILoadBalancer loadBalancer = getLoadBalancer(serviceId); //获取对应的LoadBalancer
		Server server = getServer(loadBalancer); //获取服务器,这里会执行对应的分流策略,比如轮训
                //、随机等
		if (server == null) {
			throw new IllegalStateException("No instances available for " + serviceId);
		}
		RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server,
				serviceId), serverIntrospector(serviceId).getMetadata(server));

		return execute(serviceId, ribbonServer, request);
	}
}

	protected Server getServer(ILoadBalancer loadBalancer) {
		if (loadBalancer == null) {
			return null;
		}
                // 选择对应的服务器
		return loadBalancer.chooseServer("default"); // TODO: better handling of key
	}
public class ZoneAwareLoadBalancer<T extends Server> extends DynamicServerListLoadBalancer<T> {
@Override
    public Server chooseServer(Object key) {
        if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) {
            logger.debug("Zone aware logic disabled or there is only one zone");
            return super.chooseServer(key); //默认不配置可用区,走的是这段
        }
        Server server = null;
        try {
            LoadBalancerStats lbStats = getLoadBalancerStats();
            Map<String, ZoneSnapshot> zoneSnapshot = ZoneAvoidanceRule.createSnapshot(lbStats);
            logger.debug("Zone snapshots: {}", zoneSnapshot);
            if (triggeringLoad == null) {
                triggeringLoad = DynamicPropertyFactory.getInstance().getDoubleProperty(
                        "ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".triggeringLoadPerServerThreshold", 0.2d);
            }

            if (triggeringBlackoutPercentage == null) {
                triggeringBlackoutPercentage = DynamicPropertyFactory.getInstance().getDoubleProperty(
                        "ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".avoidZoneWithBlackoutPercetage", 0.99999d);
            }
            Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, triggeringLoad.get(), triggeringBlackoutPercentage.get());
            logger.debug("Available zones: {}", availableZones);
            if (availableZones != null &&  availableZones.size() < zoneSnapshot.keySet().size()) {
                String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones);
                logger.debug("Zone chosen: {}", zone);
                if (zone != null) {
                    BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone);
                    server = zoneLoadBalancer.chooseServer(key);
                }
            }
        } catch (Exception e) {
            logger.error("Error choosing server using zone aware logic for load balancer={}", name, e);
        }
        if (server != null) {
            return server;
        } else {
            logger.debug("Zone avoidance logic is not invoked.");
            return super.chooseServer(key);
        }
    }

    //实际走到的方法
    public Server chooseServer(Object key) {
        if (counter == null) {
            counter = createCounter();
        }
        counter.increment();
        if (rule == null) {
            return null;
        } else {
            try {
                return rule.choose(key);
            } catch (Exception e) {
                logger.warn("LoadBalancer [{}]:  Error choosing server for key {}", name, key, e);
                return null;
            }
        }
    }
}

也就是说最终会调用IRule选择到一个节点,这里支持很多策略,比如随机、轮训、响应时间权重等:
image

public interface IRule{

    public Server choose(Object key);
    
    public void setLoadBalancer(ILoadBalancer lb);
    
    public ILoadBalancer getLoadBalancer();    
}

这里的LoadBalancer是在BaseLoadBalancer的构造器中设置的,上文说过,对于每一个serviceId服务来说,当第一次调用的时候会初始化对应的spring上下文,而这个上下文中包含了所有ribbon相关的bean,其中就包括ILoadBalancer、IRule。

原因

通过跟踪堆栈,发现不同的serviceId,IRule是同一个, 而上文说过,每个serviceId都拥有自己独立的上下文,包括独立的loadBalancer、IRule,而IRule是同一个,因此怀疑是这个bean是通过parent context获取到的,换句话说应用自己定义了一个这样的bean。查看代码果然如此。
这样就会导致一个问题,IRule是共享的,而其他bean是隔离开的,因此后面的serviceId初始化的时候,会修改这个IRule的LoadBalancer, 导致之前的服务获取到的实例信息是错误的,从而导致接口404。

public class BaseLoadBalancer extends AbstractLoadBalancer implements
        PrimeConnections.PrimeConnectionListener, IClientConfigAware {
    public BaseLoadBalancer() {
        this.name = DEFAULT_NAME;
        this.ping = null;
        setRule(DEFAULT_RULE);  // 这里会设置IRule的loadbalancer
        setupPingTask();
        lbStats = new LoadBalancerStats(DEFAULT_NAME);
    }
}

image

解决方案

解决方法也很简单,最简单就将这个自定义的IRule的bean干掉,另外更标准的做法是使用RibbonClients注解,具体做法可以参考文档。

总结

核心原因其实还是对于Spring Cloud的理解不够深刻,用法有错误,导致出现了一些比较诡异的问题。对于自己使用的组件、框架、甚至于每一个注解,都要了解其原理,能够清楚的说出这个注解有什么效果,有什么影响,而不是只着眼于解决眼前的问题。

Flag Counter

分布式消息队列Apache RocketMQ源码剖析-Producer分析

本文主要讲解一下阿里巴巴开源的消息队列中间件RocketMQ的producer客户端的发送流程,并简单与Kafka的实现方式做一些对比,希望能够对如何实现一个高性能网络客户端有个大致的了解。

正文

首先我们看一下Producer的继承结构:
image
MQAdmin主要包含一些管理性的接口,比如创建topic、查询某个特定消息以方便排查问题,ClientConfig主要定义了一些基本的配置,比如持久化consumer端消费offset的间隔时间(offset就是consumer端当前消费到的位置,offset的持久化机制也决定了是exactly once 还是根据时间戳等消费),然后再来看DefaultMQProducer,我们发现它将具体的实现都代理给了DefaultMQProducerImpl去做,这个类主要包含了不同的send方法,同步、异步、oneway(发出去就不管了,比如可以用于日志同步)。

    SendResult send(final Message msg) throws MQClientException, RemotingException, MQBrokerException,
        InterruptedException;

    SendResult send(final Message msg, final long timeout) throws MQClientException,
        RemotingException, MQBrokerException, InterruptedException;

    void send(final Message msg, final SendCallback sendCallback) throws MQClientException,
        RemotingException, InterruptedException;

    void send(final Message msg, final SendCallback sendCallback, final long timeout)
        throws MQClientException, RemotingException, InterruptedException;

    void sendOneway(final Message msg) throws MQClientException, RemotingException,
        InterruptedException;

通过这几个接口我们也能了解到其功能,下面来分析一下其具体实现。首先我们看一下它的主要字段以及其含义:

    //高并发下推荐替换为TheradLocalRandom,Random在高并发下会有竞争造成更高的开销
    private final Random random = new Random();
    private final DefaultMQProducer defaultMQProducer;
    //记录topic的路由信息
    private final ConcurrentMap<String/* topic */, TopicPublishInfo> topicPublishInfoTable =
        new ConcurrentHashMap<String, TopicPublishInfo>();
    private final ArrayList<SendMessageHook> sendMessageHookList = new ArrayList<SendMessageHook>();
    private final RPCHook rpcHook;
    protected BlockingQueue<Runnable> checkRequestQueue;
    protected ExecutorService checkExecutor;
    //Producer当前的状态
    private ServiceState serviceState = ServiceState.CREATE_JUST;
    private MQClientInstance mQClientFactory;
    private ArrayList<CheckForbiddenHook> checkForbiddenHookList = new ArrayList<CheckForbiddenHook>();
    //压缩等级
    private int zipCompressLevel = Integer.parseInt(System.getProperty(MixAll.MESSAGE_COMPRESS_LEVEL, "5"));

至于Producer的启动流程,可以参考之前的博客,这里主要来看一下核心的发送流程:

    public SendResult send(
        Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
        return send(msg, this.defaultMQProducer.getSendMsgTimeout());
    }

    public SendResult send(Message msg,
        long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
        //同步、异步、oneway默认都会调用sendDefaultImpl这个方法
        return this.sendDefaultImpl(msg, CommunicationMode.SYNC, null, timeout);
    }

    private SendResult sendDefaultImpl(
        Message msg, //消息体
        final CommunicationMode communicationMode,/发送方式:同步/异步/Oneway
        final SendCallback sendCallback,//回调
        final long timeout//超时时间
    ) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
        this.makeSureStateOK();
        //校验消息,比如topic名字不能超过256字节,body不能大于设置的最大值等
        Validators.checkMessage(msg, this.defaultMQProducer);

        //Random在高并发下会有竞争造成更高的开销,可替换成ThreadLocalRandom
        final long invokeID = random.nextLong();
        long beginTimestampFirst = System.currentTimeMillis();
        long beginTimestampPrev = beginTimestampFirst;
        long endTimestamp = beginTimestampFirst;
        /*
            拉取topic路由信息
         */
        TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
        if (topicPublishInfo != null && topicPublishInfo.ok()) {
            MessageQueue mq = null;
            Exception exception = null;
            SendResult sendResult = null;
            /*
                同步模式下可能会造成消息重发
             */
            int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
            int times = 0;
            //同步模式下会默认有两次重试
            String[] brokersSent = new String[timesTotal];
            for (; times < timesTotal; times++) {
                /*
                    记录上一个发送失败的broker,重试时排除掉这个有问题的broker
                     但是这里只记录了上一个,第三次重试还是会有机会轮询到第一个
                 */
                String lastBrokerName = null == mq ? null : mq.getBrokerName();
                 //选择一个要发送到topic队列,默认是轮询的方式,从而达到负载均衡
                MessageQueue tmpmq = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
                if (tmpmq != null) {
                    mq = tmpmq;
                    brokersSent[times] = mq.getBrokerName();
                    try {
                        beginTimestampPrev = System.currentTimeMillis();
                        //核心的发送方法是
                        sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout);
                        endTimestamp = System.currentTimeMillis();
                        this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);
                        switch (communicationMode) {
                            //根据不用的发送方式,进行结果的处理,异步的直接回调,返回值可以为null
                            case ASYNC:
                                return null;
                            case ONEWAY:
                                return null;
                            case SYNC:
                                if (sendResult.getSendStatus() != SendStatus.SEND_OK) {
                                    if (this.defaultMQProducer.isRetryAnotherBrokerWhenNotStoreOK()) {
                                        continue;
                                    }
                                }

                                return sendResult;
                            default:
                                break;
                        }
                    } catch (RemotingException e) {
                        endTimestamp = System.currentTimeMillis();
                        this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true);
                        log.warn(String.format("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e);
                        log.warn(msg.toString());
                        exception = e;
                        continue;
                    } catch (MQClientException e) {
                        endTimestamp = System.currentTimeMillis();
                        this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true);
                        log.warn(String.format("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e);
                        log.warn(msg.toString());
                        exception = e;
                        continue;
                    } catch (MQBrokerException e) {
                        endTimestamp = System.currentTimeMillis();
                        this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true);
                        log.warn(String.format("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e);
                        log.warn(msg.toString());
                        exception = e;
                        switch (e.getResponseCode()) {
                            case ResponseCode.TOPIC_NOT_EXIST:
                            case ResponseCode.SERVICE_NOT_AVAILABLE:
                            case ResponseCode.SYSTEM_ERROR:
                            case ResponseCode.NO_PERMISSION:
                            case ResponseCode.NO_BUYER_ID:
                            case ResponseCode.NOT_IN_CURRENT_UNIT:
                                continue;
                            default:
                                if (sendResult != null) {
                                    return sendResult;
                                }

                                throw e;
                        }
                    } catch (InterruptedException e) {
                        endTimestamp = System.currentTimeMillis();
                        this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);
                        log.warn(String.format("sendKernelImpl exception, throw exception, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e);
                        log.warn(msg.toString());

                        log.warn("sendKernelImpl exception", e);
                        log.warn(msg.toString());
                        throw e;
                    }
                } else {
                    break;
                }
            }

            if (sendResult != null) {
                return sendResult;
            }

            String info = String.format("Send [%d] times, still failed, cost [%d]ms, Topic: %s, BrokersSent: %s",
                times,
                System.currentTimeMillis() - beginTimestampFirst,
                msg.getTopic(),
                Arrays.toString(brokersSent));
            //有些异常会打出github相关的讨论issue,不过alibaba group下的issue已经删除了,
           //这个逻辑可以干掉
            info += FAQUrl.suggestTodo(FAQUrl.SEND_MSG_FAILED);

            MQClientException mqClientException = new MQClientException(info, exception);
            //异常处理,有点繁琐
            if (exception instanceof MQBrokerException) {
                mqClientException.setResponseCode(((MQBrokerException) exception).getResponseCode());
            } else if (exception instanceof RemotingConnectException) {
                mqClientException.setResponseCode(ClientErrorCode.CONNECT_BROKER_EXCEPTION);
            } else if (exception instanceof RemotingTimeoutException) {
                mqClientException.setResponseCode(ClientErrorCode.ACCESS_BROKER_TIMEOUT);
            } else if (exception instanceof MQClientException) {
                mqClientException.setResponseCode(ClientErrorCode.BROKER_NOT_EXIST_EXCEPTION);
            }

            throw mqClientException;
        }

        List<String> nsList = this.getmQClientFactory().getMQClientAPIImpl().getNameServerAddressList();
        if (null == nsList || nsList.isEmpty()) {
            throw new MQClientException(
                "No name server address, please set it." + FAQUrl.suggestTodo(FAQUrl.NAME_SERVER_ADDR_NOT_EXIST_URL), null).setResponseCode(ClientErrorCode.NO_NAME_SERVER_EXCEPTION);
        }

        throw new MQClientException("No route info of this topic, " + msg.getTopic() + FAQUrl.suggestTodo(FAQUrl.NO_TOPIC_ROUTE_INFO),
            null).setResponseCode(ClientErrorCode.NOT_FOUND_TOPIC_EXCEPTION);
    }

    private SendResult sendKernelImpl(final Message msg,
        final MessageQueue mq,
        final CommunicationMode communicationMode,
        final SendCallback sendCallback,
        final TopicPublishInfo topicPublishInfo,
        final long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
        String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
        if (null == brokerAddr) {
            /*
                找不到则尝试去抓取,因此第一次发送消息可能会有比较大延时,可以考虑再应用启动时预加载路由信息
             */
            tryToFindTopicPublishInfo(mq.getTopic());
            brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
        }

        SendMessageContext context = null;
        if (brokerAddr != null) {
//获取broker地址
            brokerAddr = MixAll.brokerVIPChannel(this.defaultMQProducer.isSendMessageWithVIPChannel(), brokerAddr);

            byte[] prevBody = msg.getBody();
            try {
                //for MessageBatch,ID has been set in the generating process
                if (!(msg instanceof MessageBatch)) {
                    MessageClientIDSetter.setUniqID(msg);
                }

                int sysFlag = 0;
                if (this.tryToCompressMessage(msg)) {
                    sysFlag |= MessageSysFlag.COMPRESSED_FLAG;
                }

                final String tranMsg = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED);
                if (tranMsg != null && Boolean.parseBoolean(tranMsg)) {
                    sysFlag |= MessageSysFlag.TRANSACTION_PREPARED_TYPE;
                }

                if (hasCheckForbiddenHook()) {
                    CheckForbiddenContext checkForbiddenContext = new CheckForbiddenContext();
                    checkForbiddenContext.setNameSrvAddr(this.defaultMQProducer.getNamesrvAddr());
                    checkForbiddenContext.setGroup(this.defaultMQProducer.getProducerGroup());
                    checkForbiddenContext.setCommunicationMode(communicationMode);
                    checkForbiddenContext.setBrokerAddr(brokerAddr);
                    checkForbiddenContext.setMessage(msg);
                    checkForbiddenContext.setMq(mq);
                    checkForbiddenContext.setUnitMode(this.isUnitMode());
                    this.executeCheckForbiddenHook(checkForbiddenContext);
                }

                if (this.hasSendMessageHook()) {
                    context = new SendMessageContext();
                    context.setProducer(this);
                    context.setProducerGroup(this.defaultMQProducer.getProducerGroup());
                    context.setCommunicationMode(communicationMode);
                    context.setBornHost(this.defaultMQProducer.getClientIP());
                    context.setBrokerAddr(brokerAddr);
                    context.setMessage(msg);
                    context.setMq(mq);
                    String isTrans = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED);
                    if (isTrans != null && isTrans.equals("true")) {
                        context.setMsgType(MessageType.Trans_Msg_Half);
                    }

                    if (msg.getProperty("__STARTDELIVERTIME") != null || msg.getProperty(MessageConst.PROPERTY_DELAY_TIME_LEVEL) != null) {
                        context.setMsgType(MessageType.Delay_Msg);
                    }
                    this.executeSendMessageHookBefore(context);
                }

                /*
                    构建消息头
                 */
                SendMessageRequestHeader requestHeader = new SendMessageRequestHeader();
                /*
                    producerGroup在非事物模式下木有啥用
                 */
                requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup());
                requestHeader.setTopic(msg.getTopic());
                requestHeader.setDefaultTopic(this.defaultMQProducer.getCreateTopicKey());
                /*
                    默认每个topic分配4个queue
                 */
                requestHeader.setDefaultTopicQueueNums(this.defaultMQProducer.getDefaultTopicQueueNums());
                /*
                    写到哪个queue
                 */
                requestHeader.setQueueId(mq.getQueueId());
                requestHeader.setSysFlag(sysFlag);
                requestHeader.setBornTimestamp(System.currentTimeMillis());
                requestHeader.setFlag(msg.getFlag());
                requestHeader.setProperties(MessageDecoder.messageProperties2String(msg.getProperties()));
                requestHeader.setReconsumeTimes(0);
                requestHeader.setUnitMode(this.isUnitMode());
                requestHeader.setBatch(msg instanceof MessageBatch);
                /*
                    重试消息
                 */
                if (requestHeader.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
                    String reconsumeTimes = MessageAccessor.getReconsumeTime(msg);
                    if (reconsumeTimes != null) {
                        requestHeader.setReconsumeTimes(Integer.valueOf(reconsumeTimes));
                        MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_RECONSUME_TIME);
                    }

                    String maxReconsumeTimes = MessageAccessor.getMaxReconsumeTimes(msg);
                    if (maxReconsumeTimes != null) {
                        requestHeader.setMaxReconsumeTimes(Integer.valueOf(maxReconsumeTimes));
                        MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_MAX_RECONSUME_TIMES);
                    }
                }

                SendResult sendResult = null;
                switch (communicationMode) {
                    case ASYNC:
                        sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage(
                            brokerAddr,
                            mq.getBrokerName(),
                            msg,
                            requestHeader,
                            timeout,
                            communicationMode,
                            sendCallback,
                            topicPublishInfo,
                            this.mQClientFactory,
                            this.defaultMQProducer.getRetryTimesWhenSendAsyncFailed(),
                            context,
                            this);
                        break;
                    case ONEWAY:
                    case SYNC:
                        sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage(
                            brokerAddr,
                            mq.getBrokerName(),
                            msg,
                            requestHeader,
                            timeout,
                            communicationMode,
                            context,
                            this);
                        break;
                    default:
                        assert false;
                        break;
                }

                if (this.hasSendMessageHook()) {
                    context.setSendResult(sendResult);
                    this.executeSendMessageHookAfter(context);
                }

                return sendResult;
            } catch (RemotingException e) {
                /*
                    这几个异常处理方式都一样,可以考虑合为一个
                 */
                if (this.hasSendMessageHook()) {
                    context.setException(e);
                    this.executeSendMessageHookAfter(context);
                }
                throw e;
            } catch (MQBrokerException e) {
                if (this.hasSendMessageHook()) {
                    context.setException(e);
                    this.executeSendMessageHookAfter(context);
                }
                throw e;
            } catch (InterruptedException e) {
                if (this.hasSendMessageHook()) {
                    context.setException(e);
                    this.executeSendMessageHookAfter(context);
                }
                throw e;
            } finally {
                msg.setBody(prevBody);
            }
        }

        throw new MQClientException("The broker[" + mq.getBrokerName() + "] not exist", null);
    }

    private boolean tryToCompressMessage(final Message msg) {
        if (msg instanceof MessageBatch) {
            //batch dose not support compressing right now
            return false;
        }
        byte[] body = msg.getBody();
        if (body != null) {
            /*
                默认大于4K压缩,不支持其他压缩方式
             */
            if (body.length >= this.defaultMQProducer.getCompressMsgBodyOverHowmuch()) {
                try {
                    byte[] data = UtilAll.compress(body, zipCompressLevel);
                    if (data != null) {
                        msg.setBody(data);
                        return true;
                    }
                } catch (IOException e) {
                    log.error("tryToCompressMessage exception", e);
                    log.warn(msg.toString());
                }
            }
        }

        return false;
    }

简单总结一下上面这坨代码:

  1. 根据topic获取其路由信息
  2. 计算重试次数
  3. 获取上次失败的broker,然后根据一个AtomicInteger的自增值,轮询topic下的所有队列,跳过上一个失败的
  4. 获取broker地址
  5. 构建SendMessageRequestHeader
  6. 发送
    这里最后一步发送会委托给MQClientAPIImpl去做,下面我们分析一下这个类的代码:
public SendResult sendMessage(
        final String addr,
        final String brokerName,
        final Message msg,
        final SendMessageRequestHeader requestHeader,
        final long timeoutMillis,
        final CommunicationMode communicationMode,
        final SendCallback sendCallback,
        final TopicPublishInfo topicPublishInfo,
        final MQClientInstance instance,
        final int retryTimesWhenSendFailed,
        final SendMessageContext context,
        final DefaultMQProducerImpl producer
    ) throws RemotingException, MQBrokerException, InterruptedException {
        RemotingCommand request = null;
        if (sendSmartMsg || msg instanceof MessageBatch) {
            SendMessageRequestHeaderV2 requestHeaderV2 = SendMessageRequestHeaderV2.createSendMessageRequestHeaderV2(requestHeader);
            request = RemotingCommand.createRequestCommand(msg instanceof MessageBatch ? RequestCode.SEND_BATCH_MESSAGE : RequestCode.SEND_MESSAGE_V2, requestHeaderV2);
        } else {
            /*
                构建消息体
             */
            request = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE, requestHeader);
        }

        request.setBody(msg.getBody());

        switch (communicationMode) {
            case ONEWAY:
                this.remotingClient.invokeOneway(addr, request, timeoutMillis);
                return null;
            case ASYNC:
                final AtomicInteger times = new AtomicInteger();
                this.sendMessageAsync(addr, brokerName, msg, timeoutMillis, request, sendCallback, topicPublishInfo, instance,
                    retryTimesWhenSendFailed, times, context, producer);
                return null;
            case SYNC:
                return this.sendMessageSync(addr, brokerName, msg, timeoutMillis, request);
            default:
                assert false;
                break;
        }

        return null;
    }

private void sendMessageAsync(
        final String addr,
        final String brokerName,
        final Message msg,
        final long timeoutMillis,
        final RemotingCommand request,
        final SendCallback sendCallback,
        final TopicPublishInfo topicPublishInfo,
        final MQClientInstance instance,
        final int retryTimesWhenSendFailed,
        final AtomicInteger times,
        final SendMessageContext context,
        final DefaultMQProducerImpl producer
    ) throws InterruptedException, RemotingException {
        /*
            异步回调
         */
        this.remotingClient.invokeAsync(addr, request, timeoutMillis, new InvokeCallback() {
            @Override
            public void operationComplete(ResponseFuture responseFuture) {
                RemotingCommand response = responseFuture.getResponseCommand();
                if (null == sendCallback && response != null) {

                    try {
                        SendResult sendResult = MQClientAPIImpl.this.processSendResponse(brokerName, msg, response);
                        if (context != null && sendResult != null) {
                            context.setSendResult(sendResult);
                            context.getProducer().executeSendMessageHookAfter(context);
                        }
                    } catch (Throwable e) {
                    }

                    producer.updateFaultItem(brokerName, System.currentTimeMillis() - responseFuture.getBeginTimestamp(), false);
                    return;
                }

                if (response != null) {
                    try {
                        SendResult sendResult = MQClientAPIImpl.this.processSendResponse(brokerName, msg, response);
                        assert sendResult != null;
                        if (context != null) {
                            context.setSendResult(sendResult);
                            context.getProducer().executeSendMessageHookAfter(context);
                        }

                        try {
                            sendCallback.onSuccess(sendResult);
                        } catch (Throwable e) {
                        }

                        producer.updateFaultItem(brokerName, System.currentTimeMillis() - responseFuture.getBeginTimestamp(), false);
                    } catch (Exception e) {
                        producer.updateFaultItem(brokerName, System.currentTimeMillis() - responseFuture.getBeginTimestamp(), true);
                        onExceptionImpl(brokerName, msg, 0L, request, sendCallback, topicPublishInfo, instance,
                            retryTimesWhenSendFailed, times, e, context, false, producer);
                    }
                } else {
                    producer.updateFaultItem(brokerName, System.currentTimeMillis() - responseFuture.getBeginTimestamp(), true);
                    if (!responseFuture.isSendRequestOK()) {
                        MQClientException ex = new MQClientException("send request failed", responseFuture.getCause());
                        onExceptionImpl(brokerName, msg, 0L, request, sendCallback, topicPublishInfo, instance,
                            retryTimesWhenSendFailed, times, ex, context, true, producer);
                    } else if (responseFuture.isTimeout()) {
                        MQClientException ex = new MQClientException("wait response timeout " + responseFuture.getTimeoutMillis() + "ms",
                            responseFuture.getCause());
                        onExceptionImpl(brokerName, msg, 0L, request, sendCallback, topicPublishInfo, instance,
                            retryTimesWhenSendFailed, times, ex, context, true, producer);
                    } else {
                        MQClientException ex = new MQClientException("unknow reseaon", responseFuture.getCause());
                        onExceptionImpl(brokerName, msg, 0L, request, sendCallback, topicPublishInfo, instance,
                            retryTimesWhenSendFailed, times, ex, context, true, producer);
                    }
                }
            }
        });
    }

这里又会去调用remotingClient,这里主要是网络IO相关的逻辑,RocketMQ采用了Netty去实现:

@Override
    public void invokeAsync(String addr, RemotingCommand request, long timeoutMillis, InvokeCallback invokeCallback)
        throws InterruptedException, RemotingConnectException, RemotingTooMuchRequestException, RemotingTimeoutException,
        RemotingSendRequestException {
        /*
            向broker建立连接
         */
        final Channel channel = this.getAndCreateChannel(addr);
        if (channel != null && channel.isActive()) {
            try {
                if (this.rpcHook != null) {
                    this.rpcHook.doBeforeRequest(addr, request);
                }
                this.invokeAsyncImpl(channel, request, timeoutMillis, invokeCallback);
            } catch (RemotingSendRequestException e) {
                log.warn("invokeAsync: send request exception, so close the channel[{}]", addr);
                this.closeChannel(addr, channel);
                throw e;
            }
        } else {
            this.closeChannel(addr, channel);
            throw new RemotingConnectException(addr);
        }
    }

private Channel getAndCreateChannel(final String addr) throws InterruptedException {
        if (null == addr)
            /*
                如果地址为空,则去抓取
             */
            return getAndCreateNameserverChannel();

        ChannelWrapper cw = this.channelTables.get(addr);
        /*
            连接正常直接返回
         */
        if (cw != null && cw.isOK()) {
            return cw.getChannel();
        }

        return this.createChannel(addr);
    }

 private Channel createChannel(final String addr) throws InterruptedException {
        ChannelWrapper cw = this.channelTables.get(addr);
        if (cw != null && cw.isOK()) {
            return cw.getChannel();
        }

        /*
            加锁保证互斥性
         */
        if (this.lockChannelTables.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {
            try {
                boolean createNewConnection = false;
                cw = this.channelTables.get(addr);
                if (cw != null) {

                    if (cw.isOK()) {
                        return cw.getChannel();
                    } else if (!cw.getChannelFuture().isDone()) {
                        createNewConnection = false;
                    } else {
                        this.channelTables.remove(addr);
                        createNewConnection = true;
                    }
                } else {
                    createNewConnection = true;
                }

                if (createNewConnection) {
                    /*
                        建立连接
                     */
                    ChannelFuture channelFuture = this.bootstrap.connect(RemotingHelper.string2SocketAddress(addr));
                    log.info("createChannel: begin to connect remote host[{}] asynchronously", addr);
                    cw = new ChannelWrapper(channelFuture);
                    this.channelTables.put(addr, cw);
                }
            } catch (Exception e) {
                log.error("createChannel: create channel exception", e);
            } finally {
                this.lockChannelTables.unlock();
            }
        } else {
            log.warn("createChannel: try to lock channel table, but timeout, {}ms", LOCK_TIMEOUT_MILLIS);
        }

        if (cw != null) {
            ChannelFuture channelFuture = cw.getChannelFuture();
            if (channelFuture.awaitUninterruptibly(this.nettyClientConfig.getConnectTimeoutMillis())) {
                if (cw.isOK()) {
                    log.info("createChannel: connect remote host[{}] success, {}", addr, channelFuture.toString());
                    return cw.getChannel();
                } else {
                    /*
                        连接失败,打印日志
                     */
                    log.warn("createChannel: connect remote host[" + addr + "] failed, " + channelFuture.toString(), channelFuture.cause());
                }
            } else {
                /*
                    超时时间内未建立连接成功
                 */
                log.warn("createChannel: connect remote host[{}] timeout {}ms, {}", addr, this.nettyClientConfig.getConnectTimeoutMillis(),
                    channelFuture.toString());
            }
        }

        return null;
    }
public abstract class NettyRemotingAbstract {
  public void invokeAsyncImpl(final Channel channel, final RemotingCommand request, final long timeoutMillis,
        final InvokeCallback invokeCallback)
        throws InterruptedException, RemotingTooMuchRequestException, RemotingTimeoutException, RemotingSendRequestException {
        final int opaque = request.getOpaque();
        /*
            流控,用于保护系统资源,semaphoreAsync相当于一个令牌,默认65535相当于没限制了
         */
        boolean acquired = this.semaphoreAsync.tryAcquire(timeoutMillis, TimeUnit.MILLISECONDS);
        if (acquired) {
            final SemaphoreReleaseOnlyOnce once = new SemaphoreReleaseOnlyOnce(this.semaphoreAsync);

            final ResponseFuture responseFuture = new ResponseFuture(opaque, timeoutMillis, invokeCallback, once);
            this.responseTable.put(opaque, responseFuture);
            try {
                /*
                    将消息写入
                 */
                channel.writeAndFlush(request).addListener(new ChannelFutureListener() {
                    @Override
                    public void operationComplete(ChannelFuture f) throws Exception {
                        if (f.isSuccess()) {
                            responseFuture.setSendRequestOK(true);
                            return;
                        } else {
                            responseFuture.setSendRequestOK(false);
                        }

                        responseFuture.putResponse(null);
                        responseTable.remove(opaque);
                        try {
                            executeInvokeCallback(responseFuture);
                        } catch (Throwable e) {
                            log.warn("excute callback in writeAndFlush addListener, and callback throw", e);
                        } finally {
                            responseFuture.release();
                        }

                        log.warn("send a request command to channel <{}> failed.", RemotingHelper.parseChannelRemoteAddr(channel));
                    }
                });
            } catch (Exception e) {
                responseFuture.release();
                log.warn("send a request command to channel <" + RemotingHelper.parseChannelRemoteAddr(channel) + "> Exception", e);
                throw new RemotingSendRequestException(RemotingHelper.parseChannelRemoteAddr(channel), e);
            }
        } else {
            if (timeoutMillis <= 0) {
                throw new RemotingTooMuchRequestException("invokeAsyncImpl invoke too fast");
            } else {
                String info =
                    String.format("invokeAsyncImpl tryAcquire semaphore timeout, %dms, waiting thread nums: %d semaphoreAsyncValue: %d",
                        timeoutMillis,
                        this.semaphoreAsync.getQueueLength(),
                        this.semaphoreAsync.availablePermits()
                    );
                log.warn(info);
                throw new RemotingTimeoutException(info);
            }
        }
    }
}

这里可以看到有几个并发的小技巧,比如RocketMQ里的ResponseFuture#executeInvokeCallback方法,通过AtomicBoolean实现方法执行exactly once语义:

    public void executeInvokeCallback() {
        if (invokeCallback != null) {
            /*
                保证callback只能执行一次
             */
            if (this.executeCallbackOnlyOnce.compareAndSet(false, true)) {
                invokeCallback.operationComplete(this);
            }
        }
    }

另外再举一个例子,比如CountDownLatch的使用,如果指定为1,那么就可以用于实现一些并发下创建连接等操作,比如线程A创建连接,线程B发现A已经在尝试建立连接了,那么B就可以阻塞在这里,等A完成后,再继续后续操作,这里依然拿ResponseFuture举例子:

public RemotingCommand waitResponse(final long timeoutMillis) throws InterruptedException {
        /*
            等待CountDownLatch.countDown执行
         */
        this.countDownLatch.await(timeoutMillis, TimeUnit.MILLISECONDS);
        return this.responseCommand;
    }

    public void putResponse(final RemotingCommand responseCommand) {
        this.responseCommand = responseCommand;
        //表明操作已经完成,唤醒await的线程
        this.countDownLatch.countDown();
    }

总结

相对来说,RocketMQ发送端这里逻辑还是比较简单的,提交一条消息后就通过Netty发送到Broker,而Kafa的会更复杂一点,Kafka这里会做一个合并,客户端提交是放到一个内存队列,然后有一个Sender线程负责根据当前的状态决定是否发送消息,这里还有一个队列会存储所有的回调,当执行完成后统计执行callback,后面有时间再写一篇博客详细分析一下Kafka的发送流程。

Flag Counter

线性一致性实现原理剖析

背景

当我们讨论分布式系统时,通常会说到CAP理论,而这里的C一致性一般来说指的就是线性一致性(Linearizability),而对于开发者来说,可能更多的会去关注自己平常使用的一些中间件、类库,比如Etcd、Zookeeper等能够提供怎么的一致性,这两个类库我们知道是在Raft、Paxos(ZAB)的基础之上实现的,因此这篇文章我们就来看一下如果线性一致性实际是如何实现的。

实现

可能大多数人都有个误解,一个分布式系统,比如ZK、etcd等,如果正确的实现了共识算法(Raft/Paxos),那么它就能够提供强一致,这个理解其实是不正确的,Raft只能够保证不同节点对于raft日志达成一致,但对于库的使用者来说,实际上对外提供服务的是底层的状态机,比如说一个KV存储,每个raft日志记录的是实际的操作,比如set a 1 set a 2等,而如果只有日志的话,我们怎么查询呢?轮训一遍日志?显然不现实,因此必须将这个状态存起来,比如RocksDB,那么底层raft日志的一致性由raft本身去保证,而上层业务方状态机的一致性该如何去保证呢?

Raft是由leader驱动的共识算法,所有的写入请求都由leader来处理,并将日志同步到follower,然后再将日志依次应用的自身的状态机,比如RocksDB,但由于网络延迟、机器负载等原因,每个节点不可能同时将日志应用到RocksDB,因此对于不同的节点来说,RocksDB的数据快照肯定不是实时一致的,并且这里会涉及到很多的corner case,比如leader切换,也就是说leader的数据也不一定是最新的,因此实际实现的时候需要考虑好这些case。
这里暂不考虑异常情况,对于raft来说,写请求都是由leader处理并同步到follower,因此leader的数据通常是最新的,但如果用户发来一个读取请求,我们直接从状态机读取的话,这里其实是会读到过期数据的,因此这里分为两步,已提交的日志 -> 已经应用到状态机的日志,因此如果不做特殊处理的话,由于还有部分日志没有应用到状态机,直接处理的话必然会造成不一致。优化的方法大致有几种:

  • 读取也走raft log
  • Read Index
  • Lease Read

读取也走raft log

我们很容易想到,让读取的请求也走一遍raft流程,由于raft日志是全局严格有序的,读写也必然是有序的,因此当处理到读取的日志的时候,能够保证之前的写入请求都已经处理完成并落到状态机,因此这个时候处理读取请求是安全的,不会造成过期读的问题,也能够满足我们说的线性一致性,但这个方法有个很明显的缺点: 性能非常低,每次读取都走一遍raft流程,涉及到网络、磁盘IO等资源,而对于大部分场景来说,都是写少读多,因此如果不对读取进行优化的话,整个类库的性能会非常低效。

Read Index

Read index读的流程这里先简单说一下,当leader接收到一个读取请求时:

  • 将当前日志的commit index记录到一个本地变量readIndex中,封装到消息体中

  • 首先需要确认自己是否仍然是leader,因此需要向其他节点都发起一次心跳

  • 如果收到了大多数节点的心跳响应,那么说明该server仍然是leader身份

  • 在状态机执行的地方,判断下apply index是否超过了readIndex,如果超过了,那么就表明发起该读取请求时,所有之前的日志都已经处理完成,也就是说能够满足线性一致性读的要求

  • 从状态机去读取结果,返回给客户端

我们可以看到,和刚刚的方法相比,read index采用心跳的方式首先确认自己仍然是leader,然后等待状态机执行到了发起读取时所有日志,就可以安全的处理客户端请求了,这里虽然还有一次心跳的网络开销,但一方面心跳包本身非常小,另外处理心跳的逻辑非常简单,比如不需要日志落盘等,因此性能相对之前的方法会高非常多。

使用了Read Index的方法,我们还可以提供follower节点读取的功能,并可以在follower上实现线性一致性读,逻辑和leader有些差异:

  • Follower向leader查询最新的readIndex

  • leader会按照上面说的,走一遍流程,但会在确认了自己leader的身份之后,直接将readIndex返回给follower

  • Follower等待自己的状态机执行到了readIndex的位置之后,就可以安全的处理客户端的读请求

    接下来我们看看sofa-jraft是如何实现Read Index的

    // 请求 ID 作为请求上下文传入
    final byte[] reqContext = new byte[4];
    Bits.putInt(reqContext, 0, requestId.incrementAndGet());
    // 调用 readIndex 方法,等待回调执行
    this.node.readIndex(reqContext, new ReadIndexClosure() {

        @Override
        public void run(Status status, long index, byte[] reqCtx) {
            if (status.isOk()) {
                //处理用户读请求
            } else {
                // 特定情况下,比如发生选举,该读请求将失败
                asyncContext.sendResponse(new BooleanCommand(false, status.getErrorMsg()));
            }
        }
    });

通过Node#readIndex(byte [] requestContext, ReadIndexClosure done)发起一次线性一致性读请求:

    private ReadOnlyService                                                readOnlyService;


    @Override
    public void readIndex(final byte[] requestContext, final ReadIndexClosure done) {
        if (this.shutdownLatch != null) {
            Utils.runClosureInThread(done, new Status(RaftError.ENODESHUTDOWN, "Node is shutting down."));
            throw new IllegalStateException("Node is shutting down");
        }
        Requires.requireNonNull(done, "Null closure");
        //异步执行,添加到ReadIndex队列中
        this.readOnlyService.addRequest(requestContext, done);
    }
public class ReadOnlyServiceImpl implements ReadOnlyService, LastAppliedLogIndexListener {

    /** Disruptor to run readonly service. */
    private Disruptor<ReadIndexEvent>                  readIndexDisruptor;
    private RingBuffer<ReadIndexEvent>                 readIndexQueue;

 @Override
    public boolean init(final ReadOnlyServiceOptions opts) {
        this.node = opts.getNode();
        this.nodeMetrics = this.node.getNodeMetrics();
        this.fsmCaller = opts.getFsmCaller();
        this.raftOptions = opts.getRaftOptions();

        this.scheduledExecutorService = Executors
                .newSingleThreadScheduledExecutor(new NamedThreadFactory("ReadOnlyService-PendingNotify-Scanner", true));
        this.readIndexDisruptor = DisruptorBuilder.<ReadIndexEvent> newInstance() //
                .setEventFactory(new ReadIndexEventFactory()) //
                .setRingBufferSize(this.raftOptions.getDisruptorBufferSize()) //
                .setThreadFactory(new NamedThreadFactory("JRaft-ReadOnlyService-Disruptor-", true)) //
                .setWaitStrategy(new BlockingWaitStrategy()) //
                .setProducerType(ProducerType.MULTI) //
                .build();
        //消费者
        this.readIndexDisruptor.handleEventsWith(new ReadIndexEventHandler());
        this.readIndexDisruptor
            .setDefaultExceptionHandler(new LogExceptionHandler<Object>(this.getClass().getSimpleName()));
        this.readIndexQueue = this.readIndexDisruptor.start();
        if(this.nodeMetrics.getMetricRegistry() != null) {
            this.nodeMetrics.getMetricRegistry().register("jraft-read-only-service-disruptor", new DisruptorMetricSet(this.readIndexQueue));
        }
        // listen on lastAppliedLogIndex change events.
        this.fsmCaller.addLastAppliedLogIndexListener(this);

        // start scanner
        this.scheduledExecutorService.scheduleAtFixedRate(() -> onApplied(this.fsmCaller.getLastAppliedIndex()),
            this.raftOptions.getMaxElectionDelayMs(), this.raftOptions.getMaxElectionDelayMs(), TimeUnit.MILLISECONDS);
        return true;
    }


    @Override
    public void addRequest(final byte[] reqCtx, final ReadIndexClosure closure) {
        if (this.shutdownLatch != null) {
            //如果节点已关闭,直接返回失败
            Utils.runClosureInThread(closure, new Status(RaftError.EHOSTDOWN, "Was stopped"));
            throw new IllegalStateException("Service already shutdown.");
        }
        try {
            EventTranslator<ReadIndexEvent> translator = (event, sequence) -> {
                event.done = closure;//回调
                event.requestContext = new Bytes(reqCtx); //请求上下文
                event.startTime = Utils.monotonicMs();//记录当前时间戳
            };
            int retryTimes = 0;
            while (true) {
                //放到队列中
                if (this.readIndexQueue.tryPublishEvent(translator)) {
                    break;
                } else {
                   //如果失败了则重试,最大3次
                    retryTimes++;
                    if (retryTimes > MAX_ADD_REQUEST_RETRY_TIMES) {
                        Utils.runClosureInThread(closure,
                            new Status(RaftError.EBUSY, "Node is busy, has too many read-only requests."));
                        this.nodeMetrics.recordTimes("read-index-overload-times", 1);
                        LOG.warn("Node {} ReadOnlyServiceImpl readIndexQueue is overload.", this.node.getNodeId());
                        return;
                    }
                    //休息一会,避免占用CPU
                    ThreadHelper.onSpinWait();
                }
            }
        } catch (final Exception e) {
            Utils.runClosureInThread(closure, new Status(RaftError.EPERM, "Node is down."));
        }
    }
}

典型的生产者、消费者模型,底层队列采用disruptor的RingBuffer, 这里主要是会合并ReadIndex请求,这里提下性能优化非常常用的一个手段:batch合并:

private class ReadIndexEventHandler implements EventHandler<ReadIndexEvent> {
        // task list for batch
        private final List<ReadIndexEvent> events = new ArrayList<>(
                                                      ReadOnlyServiceImpl.this.raftOptions.getApplyBatch());

        @Override
        public void onEvent(final ReadIndexEvent newEvent, final long sequence, final boolean endOfBatch)
                                                                                                         throws Exception {
            if (newEvent.shutdownLatch != null) {
                executeReadIndexEvents(this.events);
                this.events.clear();
                newEvent.shutdownLatch.countDown();
                return;
            }

            this.events.add(newEvent);
            //合并ReadIndex请求,默认32个
            if (this.events.size() >= ReadOnlyServiceImpl.this.raftOptions.getApplyBatch() || endOfBatch) {
                executeReadIndexEvents(this.events);
                this.events.clear();
            }
        }
    }

   private void executeReadIndexEvents(final List<ReadIndexEvent> events) {
        if (events.isEmpty()) {
            return;
        }
        //构造消息体
        final ReadIndexRequest.Builder rb = ReadIndexRequest.newBuilder() //
            .setGroupId(this.node.getGroupId()) //
            .setServerId(this.node.getServerId().toString());

        final List<ReadIndexState> states = new ArrayList<>(events.size());

        for (final ReadIndexEvent event : events) {
            rb.addEntries(ZeroByteStringHelper.wrap(event.requestContext.get()));
            states.add(new ReadIndexState(event.requestContext, event.done, event.startTime));
        }
        final ReadIndexRequest request = rb.build();
        
        this.node.handleReadIndexRequest(request, new ReadIndexResponseClosure(states, request));
    }

可以看到,实际处理ReadIndex请求的是Node#handleReadIndexRequest, 这里注意,会在上层进行合并,另外这里传入了一个ReadIndexResponseClosure回调,这个回调会在节点确认了自己leader的身份之后执行

   /**
     * Handle read index request.
     */
    @Override
    public void handleReadIndexRequest(final ReadIndexRequest request, final RpcResponseClosure<ReadIndexResponse> done) {
        final long startMs = Utils.monotonicMs();
        this.readLock.lock();
        try {
            switch (this.state) {
                //如果是leader的话,直接处理即可
                case STATE_LEADER:
                    readLeader(request, ReadIndexResponse.newBuilder(), done);
                    break;
                case STATE_FOLLOWER:
                    //如果该节点是follower的话,需要向leader查询readIndex
                    readFollower(request, done);
                    break;
                case STATE_TRANSFERRING:
                    //如果leader正在迁移,则直接返回
                    done.run(new Status(RaftError.EBUSY, "Is transferring leadership."));
                    break;
                default:
                    done.run(new Status(RaftError.EPERM, "Invalid state for readIndex: %s.", this.state));
                    break;
            }
        } finally {
            this.readLock.unlock();
            this.metrics.recordLatency("handle-read-index", Utils.monotonicMs() - startMs);
            this.metrics.recordSize("handle-read-index-entries", request.getEntriesCount());
        }
    }

//如果是follower的话,需要向leader查询readIndex
private void readFollower(final ReadIndexRequest request, final RpcResponseClosure<ReadIndexResponse> closure) {
        if (this.leaderId == null || this.leaderId.isEmpty()) {
            closure.run(new Status(RaftError.EPERM, "No leader at term %d.", this.currTerm));
            return;
        }
        // send request to leader.
        final ReadIndexRequest newRequest = ReadIndexRequest.newBuilder() //
            .mergeFrom(request) //
            .setPeerId(this.leaderId.toString()) //
            .build();
        this.rpcService.readIndex(this.leaderId.getEndpoint(), newRequest, -1, closure);
    }

    private void readLeader(final ReadIndexRequest request, final ReadIndexResponse.Builder respBuilder,
                            final RpcResponseClosure<ReadIndexResponse> closure) {
        final int quorum = getQuorum();
        if (quorum <= 1) {
            // Only one peer, fast path.
            respBuilder.setSuccess(true) //
                .setIndex(this.ballotBox.getLastCommittedIndex());
            closure.setResponse(respBuilder.build());
            closure.run(Status.OK());
            return;
        }
        
        //记录当前日志的commit index,即readIndex
        final long lastCommittedIndex = this.ballotBox.getLastCommittedIndex();
        //leader刚启动时,需要先提交一条日志,确认自己的leader身份
        if (this.logManager.getTerm(lastCommittedIndex) != this.currTerm) {
            // Reject read only request when this leader has not committed any log entry at its term
            closure
                .run(new Status(
                    RaftError.EAGAIN,
                    "ReadIndex request rejected because leader has not committed any log entry at its term, logIndex=%d, currTerm=%d.",
                    lastCommittedIndex, this.currTerm));
            return;
        }
        respBuilder.setIndex(lastCommittedIndex);
        
        //如果该请求来自follower,需要确认下该follower是否仍然在该raft集群中
        if (request.getPeerId() != null) {
            // request from follower, check if the follower is in current conf.
            final PeerId peer = new PeerId();
            peer.parse(request.getServerId());
            if (!this.conf.contains(peer)) {
                closure
                    .run(new Status(RaftError.EPERM, "Peer %s is not in current configuration: {}.", peer, this.conf));
                return;
            }
        }
        
        ReadOnlyOption readOnlyOpt = this.raftOptions.getReadOnlyOptions();
        if (readOnlyOpt == ReadOnlyOption.ReadOnlyLeaseBased && !isLeaderLeaseValid()) {
            // If leader lease timeout, we must change option to ReadOnlySafe
            readOnlyOpt = ReadOnlyOption.ReadOnlySafe;
        }

        switch (readOnlyOpt) {
            case ReadOnlySafe:
                final List<PeerId> peers = this.conf.getConf().getPeers();
                Requires.requireTrue(peers != null && !peers.isEmpty(), "Empty peers");
                final ReadIndexHeartbeatResponseClosure heartbeatDone = new ReadIndexHeartbeatResponseClosure(closure,
                    respBuilder, quorum, peers.size());
                // Send heartbeat requests to followers
                for (final PeerId peer : peers) {
                    if (peer.equals(this.serverId)) {
                        continue;
                    }
                    //向所有其他的节点发送心跳包
                    this.replicatorGroup.sendHeartbeat(peer, heartbeatDone);
                }
                break;
            case ReadOnlyLeaseBased:
                // Responses to followers and local node.
                respBuilder.setSuccess(true);
                closure.setResponse(respBuilder.build());
                closure.run(Status.OK());
                break;
        }
    }

private class ReadIndexHeartbeatResponseClosure extends RpcResponseClosureAdapter<AppendEntriesResponse> {
        final ReadIndexResponse.Builder             respBuilder;
        final RpcResponseClosure<ReadIndexResponse> closure;
        final int                                   quorum;
        final int                                   failPeersThreshold;
        int                                         ackSuccess;
        int                                         ackFailures;
        boolean                                     isDone;

        public ReadIndexHeartbeatResponseClosure(final RpcResponseClosure<ReadIndexResponse> closure,
                                                 final ReadIndexResponse.Builder rb, final int quorum,
                                                 final int peersCount) {
            super();
            this.closure = closure;
            this.respBuilder = rb;
            this.quorum = quorum;
            this.failPeersThreshold = peersCount % 2 == 0 ? (quorum - 1) : quorum;
            this.ackSuccess = 0;
            this.ackFailures = 0;
            this.isDone = false;
        }

        @Override
        public synchronized void run(final Status status) {
            if (this.isDone) {
                return;
            }
            if (status.isOk() && getResponse().getSuccess()) {
                this.ackSuccess++;
            } else {
                this.ackFailures++;
            }
            // 如果收到了大多数节点的心跳包,那么说明该节点仍然是leader
            // 执行回调
            if (this.ackSuccess + 1 >= this.quorum) {
                this.respBuilder.setSuccess(true);
                this.closure.setResponse(this.respBuilder.build());
                this.closure.run(Status.OK());
                this.isDone = true;
            } else if (this.ackFailures >= this.failPeersThreshold) {
                this.respBuilder.setSuccess(false);
                this.closure.setResponse(this.respBuilder.build());
                this.closure.run(Status.OK());
                this.isDone = true;
            }
        }
    }

在确认了leader的身份之后,需要等待状态机执行到readIndex:

class ReadIndexResponseClosure extends RpcResponseClosureAdapter<ReadIndexResponse> {

        final List<ReadIndexState> states;
        final ReadIndexRequest     request;

        public ReadIndexResponseClosure(final List<ReadIndexState> states, final ReadIndexRequest request) {
            super();
            this.states = states;
            this.request = request;
        }

        /**
         * Called when ReadIndex response returns.
         */
        @Override
        public void run(final Status status) {
            if (!status.isOk()) {
                notifyFail(status);
                return;
            }
            final ReadIndexResponse readIndexResponse = getResponse();
            if (!readIndexResponse.getSuccess()) {
                notifyFail(new Status(-1, "Fail to run ReadIndex task, maybe the leader stepped down."));
                return;
            }
            // Success
            final ReadIndexStatus readIndexStatus = new ReadIndexStatus(this.states, this.request,
                readIndexResponse.getIndex());
            for (final ReadIndexState state : this.states) {
                // Records current commit log index.
                state.setIndex(readIndexResponse.getIndex());
            }

            boolean doUnlock = true;
            ReadOnlyServiceImpl.this.lock.lock();
            try {
	              //如果状态机已经执行到了readIndex,那么说明可以处理用户的请求了
                if (readIndexStatus.isApplied(ReadOnlyServiceImpl.this.fsmCaller.getLastAppliedIndex())) {
                    // Already applied,notify readIndex request.
                    ReadOnlyServiceImpl.this.lock.unlock();
                    doUnlock = false;
                    notifySuccess(readIndexStatus);
                } else {
                    // 状态机还没有执行到readIndex,需要放到pending队列
                    ReadOnlyServiceImpl.this.pendingNotifyStatus
                    .computeIfAbsent(readIndexStatus.getIndex(), k -> new ArrayList<>(10)).add(readIndexStatus);
                }
            } finally {
                if (doUnlock) {
                    ReadOnlyServiceImpl.this.lock.unlock();
                }
            }
        }

        private void notifyFail(final Status status) {
            final long nowMs = Utils.monotonicMs();
            for (final ReadIndexState state : this.states) {
                ReadOnlyServiceImpl.this.nodeMetrics.recordLatency("read-index", nowMs - state.getStartTimeMs());
                final ReadIndexClosure done = state.getDone();
                if (done != null) {
                    final Bytes reqCtx = state.getRequestContext();
                    done.run(status, ReadIndexClosure.INVALID_LOG_INDEX, reqCtx != null ? reqCtx.get() : null);
                }
            }
        }
    }

Lease Read

虽然 ReadIndex Read 比第一种方法快很多,但我们发现还是有一次心跳包的网络开销,raft论文里提到了一种更激进的方式,也就是leader其实都存在一个'在为期', 也就是说leader会向follower发送心跳包,当收到大多数节点的回复后即确认了leader身份,并记录下时间戳t1,如果读过raft论文我们会知道,follower会在Election Timeout超时后,才会认为leader挂掉,并发起一次新的选举,也就是说在下一次leader选举肯定发生在t1 + Election Timeout +/- 时钟偏移量之后,因此第二种方法的中,通过发送心跳包确认leader身份这一步就可以省略掉了,在Election Timeout内只需要确认一次即可,相对一种方式,不仅仅不需要写raft日志,连心跳包都省略掉了,可想而知,性能会大大提升,但这里存在一个潜在的问题是,服务器的时间可能会偏移,存在一定的误差,如果偏移过大的话,这种机制就不够安全。

Zookeeper的一致性保证

Zookeeper是基于ZAB(类Paxos)协议实现的,我们可能常常有一个误解,就是ZK能够提供强一致,但其实默认情况下ZK并不能提供全局一致的数据视图,或者说线性一致性,比如说有两个客户端A和B,如果客户端将一个节点a的值从0改成1,并告诉客户端B去读取a,由于客户端连接的zk服务器不同,可能读取到0,也可能读到1。但如果你需要更强的一致性,期望让A和B读取到同样的值,可以通过执行Zookeeper#sync()方法,并在回调中读取a的值。
也就是说默认情况下,zk并不能保证线性一致性读,也就是由于网络延迟、gc等原因,不同的节点接受到日志的时间不一致、应用到本地内存数据库的时间也有差异,但对于读取请求来说,每一个zk服务器都会对外提供服务,即使是follower也是一样,也就是说follower有一层本地缓存,这也是zk读取性能很高的一个原因,如果需要更高的读取性能,我们可以增加更多的follower节点,均衡读取的压力,但要注意增加更多的节点,也意味着写请求需要同步到更多的节点,也就是写入的性能会下降,这个需要业务方自己去权衡。但如果业务方有线性一致性读的要求,可以通过sync调用,基本原理其实和read index差不多,来客户端连接的zk服务器本地的内存数据库追上最新的日志。
和读取不同,写入请求(set/delete)是满足线性一致性写的,也就是写入必须要走一次ZAB流程,但读取,由于每个客户端连接的zk服务节点不一致,而每个zk服务节点都会对外提供写服务,才会导致可能读到过期值。

总结

本文围绕raft以及zk的线性一致性实现细节以及相关的原理, 这里其实还涉及到很多的细节,后面会在单独的博客中补充。

反射和泛型简析

前言

泛型是在JDK1.5中才引入到Java的,
然而反射在JDK1.1就已经存在了,因此不可避免的出现了一些因为兼容性等原因,造成的一些容易令人疑惑的问题

正文

假如我们有一个Father类:

public class Father {
    public Father() {
        System.out.println("Father");
    }
}

子类Son继承Father类:

public class Son extends Father {
    public Song() {
        System.out.println("Son");
    }
}

再写一个测试类Main:

public class Main {
   Class<Son> sonClass = Son.class;
   Class<Father> fatherClass = sonClass.getSuperclass();//InCompatible Type
   Class<? super Son> fatherClass = sonClass.getSuperclass();//只能写成这种形式

}

但是这段代码却不能通过编译,编译器会告诉我们类型不兼容,这很令人疑惑,因为Java是单继承,这里很明确的Son的父类就是Father,问题就在于反射是在运行时加载类,而泛型是个编译时特性,在运行时会被擦除,
因此对于编译器来说,没有足够的信息去检查运行时类型是否正确,只能够根据方法的返回值类型去检查,比如:Class.newInstance()的返回值是T,因此就能够根据泛型的信息直接得到确切的类型,而 Object father = fatherClass.newInstance();
却只能得到Object类型

Class<Integer> integerClass  = Integer.class;
Integer i = integerClass.newInstance();
 public T newInstance()throws InstantiationException, IllegalAccessException {
		...			
 }

而getSupperclass()的方法签名为 public native Class<? super T> getSuperclass();,这时候编译器只知道返回值是T的父类但不知道确切的类型,因此当你用Class的时候会得到一个编译错误.

Flag Counter

Redis用于频率限制上踩过的坑

背景

今天分享下前段时间遇到的一个case,相信大家都有做过类似频率限制的东西,我们的也有类似的业务场景,某个接口或者功能需要限制用户一段时间内的访问量,我们的解决方案是通过Redis去做,一方面是由于Redis完全是内存访问性能比较高,另一方面系统是分布式的,如果是单机的或者说只需要限制单机访问的QPS那么可以采用GuavaRateLimiter

现象

比如有这么一个场景,接口A限制用户30S内只能调用3次,但出现了一个诡异的现象是,已经过了这个时间还是不能调用,查看应用日志、外部依赖都没有发现异常。

问题定位

首先看一下应用最近有没有发布过,是不是新功能导致的,然而并没有。因为这段代码最近一直没有改动,而且一直没遇到过类似的问题,因此开始怀疑代码逻辑有漏洞,一层一层拨开迷雾,找到最核心的代码,伪码如下:

Jedis redis = getRedis();
try {
    redis.set(SafeEncoder.encode(key), SafeEncoder.encode(def + ""), "nx".getBytes(),
    "ex".getBytes(), exp);
    Long count = redis.incrBy(key.getBytes(), val);
} finally {
    redis.close();
}

做的事情很简单,第一set命令就是说若key不存在则将值设置为def,并且设置过期时间,然后incrBy命令自增val,因此这里如果val传递了0则可以获取当前值,但是这里其实有一个问题,不是很容易复现,但是一旦出现用户就不能调用接口了。

问题

假设应用在调用这个方法,在时间点t1执行set命令,并发现key是存在的,那么就不会设置过期时间,也不会去设置默认值,然后再时间点t2调用incrBy命令,但是如果这里key刚好在t1和t2之间过期的话,那么这个key就会一直存在,也就会导致上述的问题。

  1. 客户端执行set命令,这个时候key还未过期,因此set命令不会设置value也不会设置过期时间
  2. set命令执行完毕,这个时候key过期
  3. 客户端执行incrBy命令,因为上一步中key已经过期,因此这里的incrBy命令相当于在一个新的key上自增,但这里的关键是没有设置过期时间,也就是说key会一直存在。

解决方案

这里提出一种解决方案,首先分析一下这段代码想做什么,传递一个key和默认值以及一个过期时间,需求就是自增并且能够过期。那么分析之后发现其实不需要set命令,下面给出一个解决方案:

try (Jedis redis = getRedis()) {
	Long count = redis.incrBy(key.getBytes(), val);
	if (count == val) {
	    redis.expire(key, exp);
	}
}

首先调用incrBy命令自增,如果incrBy返回的值等于val,那么说明这是第一次调用因此需要设置下过期时间。
但其实这里还是有个问题,如果incrBy和expire这两个命令执行之间发生了异常,比如连接断掉等,但是incrBy命令执行成功了,而expire没有得到执行,那么这个key也会永远存在,因为代码设置过期时间的条件是第一次自增的时候, 但这个概率一般来说非常小了,如果想避免类似的情况发生,最好改成lua脚本,我们知道lua脚本执行时原子的,而且之前的方案涉及到了两次网络调用,而改成lua脚本这样就只有一次网络调用,如果还想优化那么可以改成evalsha命令,避免每次都需要传递lua脚本避免额外的网络开销。当然这里其实还有很多其他的方案,这里只是给出一种方案。

经验教训

分布式、高并发系统是一个很复杂的领域,编写相关的代码也需要更好的意识,写完代码后,我们需要仔细分析下代码在各种case下的表现,比如其中一个服务超时了,这个时候如何处理,是重试还是直接往上层抛异常等,以及代码在高并发下会如何表现等等。
我的建议是多多阅读优秀的代码,多思考他们是如何处理各种case的,包括日志、异常的处理等等,多学习、多踩坑才能更快的成长。

Flag Counter

Java并发工具类之LongAdder原理总结

java.util.concurrency.atomic.LongAdder是Java8新增的一个类,提供了原子累计值的方法。根据文档的描述其性能要优于AtomicLong,下图是一个简单的测试对比(平台:MBP):
image
这里测试时基于JDK1.8进行的,AtomicLong 从Java8开始针对x86平台进行了优化,使用XADD替换了CAS操作,我们知道JUC下面提供的原子类都是基于Unsafe类实现的,并由Unsafe来提供CAS的能力。CAS (compare-and-swap)本质上是由现代CPU在硬件级实现的原子指令,允许进行无阻塞,多线程的数据操作同时兼顾了安全性以及效率。大部分情况下,CAS都能够提供不错的性能,但是在高竞争的情况下开销可能会成倍增长,具体的研究可以参考这篇文章, 我们直接看下代码:

public class AtomicLong {
public final long incrementAndGet() {
        return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
    }
}

public final class Unsafe {
public final long getAndAddLong(Object var1, long var2, long var4) {
        long var6;
        do {
            var6 = this.getLongVolatile(var1, var2);
        } while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));
        return var6;
    }
}

getAndAddLong方法会以volatile的语义去读需要自增的域的最新值,然后通过CAS去尝试更新,正常情况下会直接成功后返回,但是在高并发下可能会同时有很多线程同时尝试这个过程,也就是说线程A读到的最新值可能实际已经过期了,因此需要在while循环中不断的重试,造成很多不必要的开销,而xadd的相对来说会更高效一点,伪码如下,最重要的是下面这段代码是原子的,也就是说其他线程不能打断它的执行或者看到中间值,这条指令是在硬件级直接支持的:

function FetchAndAdd(address location, int inc) {
    int value := *location
    *location := value + inc
    return value
}

而LongAdder的性能比上面那种还要好很多,于是就研究了一下。首先它有一个基础的值base,在发生竞争的情况下,会有一个Cell数组用于将不同线程的操作离散到不同的节点上去(会根据需要扩容,最大为CPU核数),sum()会将所有Cell数组中的value和base累加作为返回值。核心的**就是将AtomicLong一个value的更新压力分散到多个value中去,从而降低更新热点。

public class LongAdder extends Striped64 implements Serializable {
//...
}

LongAdder继承自Striped64Striped64内部维护了一个懒加载的数组以及一个额外的base实例域,数组的大小是2的N次方,使用每个线程Thread内部的哈希值访问。

abstract class Striped64 extends Number {
/** Number of CPUS, to place bound on table size */
    static final int NCPU = Runtime.getRuntime().availableProcessors();

    /**
     * Table of cells. When non-null, size is a power of 2.
     */
    transient volatile Cell[] cells;
     
@sun.misc.Contended static final class Cell {
        volatile long value;
        Cell(long x) { value = x; }
        final boolean cas(long cmp, long val) {
            return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
        }

        // Unsafe mechanics
        private static final sun.misc.Unsafe UNSAFE;
        private static final long valueOffset;
        static {
            try {
                UNSAFE = sun.misc.Unsafe.getUnsafe();
                Class<?> ak = Cell.class;
                valueOffset = UNSAFE.objectFieldOffset
                    (ak.getDeclaredField("value"));
            } catch (Exception e) {
                throw new Error(e);
            }
        }
    }

}

数组的元素是Cell类,可以看到Cell类用Contended注解修饰,这里主要是解决false sharing(伪共享的问题),不过个人认为伪共享翻译的不是很好🌶🐓,或者应该是错误的共享,比如两个volatile变量被分配到了同一个缓存行,但是这两个的更新在高并发下会竞争,比如线程A去更新变量a,线程B去更新变量b,但是这两个变量被分配到了同一个缓存行,因此会造成每个线程都去争抢缓存行的所有权,例如A获取了所有权然后执行更新这时由于volatile的语义会造成其刷新到主存,但是由于变量b也被缓存到同一个缓存行,因此就会造成cache miss,这样就会造成极大的性能损失,因此有一些类库的作者,例如JUC下面的、Disruptor等都利用了插入dummy 变量的方式,使得缓存行被其独占,比如下面这种代码:

static final class Cell {
        volatile long p0, p1, p2, p3, p4, p5, p6;
        volatile long value;
        volatile long q0, q1, q2, q3, q4, q5, q6;
        Cell(long x) { value = x; }

        final boolean cas(long cmp, long val) {
            return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
        }

        // Unsafe mechanics
        private static final sun.misc.Unsafe UNSAFE;
        private static final long valueOffset;
        static {
            try {
                UNSAFE = getUnsafe();
                Class<?> ak = Cell.class;
                valueOffset = UNSAFE.objectFieldOffset
                    (ak.getDeclaredField("value"));
            } catch (Exception e) {
                throw new Error(e);
            }
        }
 }

但是这种方式毕竟不通用,例如32、64位操作系统的缓存行大小不一样,因此JAVA8中就增加了一个注@sun.misc.Contended解用于解决这个问题,由JVM去插入这些变量,具体可以参考openjdk.java.net/jeps/142 ,但是通常来说对象是不规则的分配到内存中的,但是数组由于是连续的内存,因此可能会共享缓存行,因此这里加一个Contended注解以防cells数组发生伪共享的情况。

/**
 * 底竞争下直接更新base,类似AtomicLong
 * 高并发下,会将每个线程的操作hash到不同的
 * cells数组中,从而将AtomicLong中更新
 * 一个value的行为优化之后,分散到多个value中
 * 从而降低更新热点,而需要得到当前值的时候,直接
 * 将所有cell中的value与base相加即可,但是跟
 * AtomicLong(compare and change -> xadd)的CAS不同,
 * incrementAndGet操作及其变种
 * 可以返回更新后的值,而LongAdder返回的是void
 */
public class LongAdder {
    public void add(long x) {
        Cell[] as; long b, v; int m; Cell a;
        /**
         *  如果是第一次执行,则直接case操作base
         */
        if ((as = cells) != null || !casBase(b = base, b + x)) {
            boolean uncontended = true;
            /**
             * as数组为空(null或者size为0)
             * 或者当前线程取模as数组大小为空
             * 或者cas更新Cell失败
             */
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[getProbe() & m]) == null ||
                !(uncontended = a.cas(v = a.value, v + x)))
                longAccumulate(x, null, uncontended);
        }
    }

    public long sum() {
       //通过累加base与cells数组中的value从而获得sum
        Cell[] as = cells; Cell a;
        long sum = base;
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
    }
}

/**
 * openjdk.java.net/jeps/142
 */
@sun.misc.Contended static final class Cell {
    volatile long value;
    Cell(long x) { value = x; }
    final boolean cas(long cmp, long val) {
        return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
    }

    // Unsafe mechanics
    private static final sun.misc.Unsafe UNSAFE;
    private static final long valueOffset;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> ak = Cell.class;
            valueOffset = UNSAFE.objectFieldOffset
                (ak.getDeclaredField("value"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
}

abstract class Striped64 extends Number {

    final void longAccumulate(long x, LongBinaryOperator fn,
                              boolean wasUncontended) {
        int h;
        if ((h = getProbe()) == 0) {
            /**
             * 若getProbe为0,说明需要初始化
             */
            ThreadLocalRandom.current(); // force initialization
            h = getProbe();
            wasUncontended = true;
        }
        boolean collide = false;                // True if last slot nonempty
        /**
         * 失败重试
         */
        for (;;) {
            Cell[] as; Cell a; int n; long v;
            if ((as = cells) != null && (n = as.length) > 0) {
                /**
                 *  若as数组已经初始化,(n-1) & h 即为取模操作,相对 % 效率要更高
                 */
                if ((a = as[(n - 1) & h]) == null) {
                    if (cellsBusy == 0) {       // Try to attach new Cell
                        Cell r = new Cell(x);   // Optimistically create
                        if (cellsBusy == 0 && casCellsBusy()) {//这里casCellsBusy的作用其实就是一个spin lock
                            //可能会有多个线程执行了`Cell r = new Cell(x);`,
                            //因此这里进行cas操作,避免线程安全的问题,同时前面在判断一次
                            //避免正在初始化的时其他线程再进行额外的cas操作
                            boolean created = false;
                            try {               // Recheck under lock
                                Cell[] rs; int m, j;
                                //重新检查一下是否已经创建成功了
                                if ((rs = cells) != null &&
                                    (m = rs.length) > 0 &&
                                    rs[j = (m - 1) & h] == null) {
                                    rs[j] = r;
                                    created = true;
                                }
                            } finally {
                                cellsBusy = 0;
                            }
                            if (created)
                                break;
                            continue;           // Slot 现在是非空了,continue到下次循环重试
                        }
                    }
                    collide = false;
                }
                else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;      // Continue after rehash
                else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                             fn.applyAsLong(v, x))))
                    break;//若cas更新成功则跳出循环,否则继续重试
                else if (n >= NCPU || cells != as) // 最大只能扩容到CPU数目, 或者是已经扩容成功,这里只有的本地引用as已经过期了
                    collide = false;            // At max size or stale
                else if (!collide)
                    collide = true;
                else if (cellsBusy == 0 && casCellsBusy()) {
                    try {
                        if (cells == as) {      // 扩容
                            Cell[] rs = new Cell[n << 1];
                            for (int i = 0; i < n; ++i)
                                rs[i] = as[i];
                            cells = rs;
                        }
                    } finally {
                        cellsBusy = 0;
                    }
                    collide = false;
                    continue;                   // Retry with expanded table
                }
                //重新计算hash(异或)从而尝试找到下一个空的slot
                h = advanceProbe(h);
            }
            else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
                boolean init = false;
                try {                           // Initialize table
                    if (cells == as) {
                        /**
                         * 默认size为2
                         */
                        Cell[] rs = new Cell[2];
                        rs[h & 1] = new Cell(x);
                        cells = rs;
                        init = true;
                    }
                } finally {
                    cellsBusy = 0;
                }
                if (init)
                    break;
            }
            else if (casBase(v = base, ((fn == null) ? v + x : // 若已经有另一个线程在初始化,那么尝试直接更新base
                                        fn.applyAsLong(v, x))))
                break;                          // Fall back on using base
        }
    }

    final boolean casCellsBusy() {
        return UNSAFE.compareAndSwapInt(this, CELLSBUSY, 0, 1);
    }

    static final int getProbe() {
        /**
         * 通过Unsafe获取Thread中threadLocalRandomProbe的值
         */
        return UNSAFE.getInt(Thread.currentThread(), PROBE);
    }

        // Unsafe mechanics
        private static final sun.misc.Unsafe UNSAFE;
        private static final long BASE;
        private static final long CELLSBUSY;
        private static final long PROBE;
        static {
            try {
                UNSAFE = sun.misc.Unsafe.getUnsafe();
                Class<?> sk = Striped64.class;
                BASE = UNSAFE.objectFieldOffset
                    (sk.getDeclaredField("base"));
                CELLSBUSY = UNSAFE.objectFieldOffset
                    (sk.getDeclaredField("cellsBusy"));
                Class<?> tk = Thread.class;
                //返回Field在内存中相对于对象内存地址的偏移量
                PROBE = UNSAFE.objectFieldOffset
                    (tk.getDeclaredField("threadLocalRandomProbe"));
            } catch (Exception e) {
                throw new Error(e);
            }
        }
}

由于Cell相对来说比较占内存,因此这里采用懒加载的方式,在无竞争的情况下直接更新base域,在第一次发生竞争的时候(CAS失败)就会创建一个大小为2的cells数组,每次扩容都是加倍,只到达到CPU核数。同时我们知道扩容数组等行为需要只能有一个线程同时执行,因此需要一个锁,这里通过CAS更新cellsBusy来实现一个简单的spin lock。
数组访问索引是通过Thread里的threadLocalRandomProbe域取模实现的,这个域是ThreadLocalRandom更新的,cells的数组大小被限制为CPU的核数,因为即使有超过核数个线程去更新,但是每个线程也只会和一个CPU绑定,更新的时候顶多会有cpu核数个线程,因此我们只需要通过hash将不同线程的更新行为离散到不同的slot即可。
我们知道线程、线程池会被关闭或销毁,这个时候可能这个线程之前占用的slot就会变成没人用的,但我们也不能清除掉,因为一般web应用都是长时间运行的,线程通常也会动态创建、销毁,很可能一段时间后又会被其他线程占用,而对于短时间运行的,例如单元测试,清除掉有啥意义呢?

总结

总的来说,LongAdder从性能上来说要远远好于AtomicLong,一般情况下是可以直接替代AtomicLong使用的,Netty也通过一个接口封装了这两个类,在Java8下直接采用LongAdder,但是AtomicLong的一系列方法不仅仅可以自增,还可以获取更新后的值,如果是例如获取一个全局唯一的ID还是采用AtomicLong会方便一点。

参考链接

  1. https://blogs.oracle.com/dave/atomic-fetch-and-add-vs-compare-and-swap
  2. https://en.wikipedia.org/wiki/Compare-and-swap
  3. http://ashkrit.blogspot.com/2014/02/atomicinteger-java-7-vs-java-8.html
  4. https://dzone.com/articles/adventures-atomiclong

Flag Counter

Kotlin coroutine详解

前言

本文主要介绍一下Kotlin是如何实现Coroutine的,对于具体的用法推荐参考一下官方文档,讲得还是比较详细的

什么是 Coroutine

概念上来说类似于线程,拥有自己的栈、本地变量、指令指针等,需要一段代码块来运行并且拥有类似的生命周期。但是和线程不同,coroutine并不和某一个特定的线程绑定,它可以在线程A中执行,并在某一个时刻暂停(suspend),等下次调度到恢复执行的时候在线程B中执行。不同于线程,coroutine是协作式的,即子程序可以通过在函数中有不同的入口点来实现暂停、恢复,从而让出线程资源。

实战演练

首先看一个简单的小demo,来看看Kotlin的Coroutine是具体适合使用的:

    @Test
    fun async() {
        async {
            delay(1000)
            print("World!")
        }
        print("Hello ")
        Thread.sleep(2000)
    }

上面这段代码会输出Hello World!, 那么下面我们看看具体是如何工作的.

原理剖析

asyn()这里是一个函数,下面是它的源码:

public val DefaultDispatcher: CoroutineDispatcher = CommonPool

public fun <T> async(
    context: CoroutineContext = DefaultDispatcher,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T> {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyDeferredCoroutine(newContext, block) else
        DeferredCoroutine<T>(newContext, active = true)
    coroutine.initParentJob(context[Job])
    start(block, coroutine, coroutine)
    return coroutine
}

这个函数有三个参数,其中两个都有默认值,也就是默认不需要传递,context是指coroutine的上下文,默认是DefaultDispatcher,DefaultDispatcher当前的实现是CommonPool,由于目前还是experimental,后面说不定会更改成其他的实现。

object CommonPool : CoroutineDispatcher() {
    private var usePrivatePool = false

    @Volatile
    private var _pool: Executor? = null

    private inline fun <T> Try(block: () -> T) = try { block() } catch (e: Throwable) { null }

    private fun createPool(): ExecutorService {
        val fjpClass = Try { Class.forName("java.util.concurrent.ForkJoinPool") }
            ?: return createPlainPool()
        if (!usePrivatePool) {
            Try { fjpClass.getMethod("commonPool")?.invoke(null) as? ExecutorService }
                ?.let { return it }
        }
        Try { fjpClass.getConstructor(Int::class.java).newInstance(defaultParallelism()) as? ExecutorService }
            ?. let { return it }
        return createPlainPool()
    }

    private fun createPlainPool(): ExecutorService {
        val threadId = AtomicInteger()
        return Executors.newFixedThreadPool(defaultParallelism()) {
            Thread(it, "CommonPool-worker-${threadId.incrementAndGet()}").apply { isDaemon = true }
        }
    }
}

CommonPool默认会使用ForkJoinPool作为coroutine的调度策略,如果不存在则fallback到线程池的策略,ForkJoinPool实现了work-stealing算法,当前线程的工作完成后从其他线程的待执行任务中窃取,具体的解释推荐直接看其注释,比网上的博客清晰的多。
而第二个参数CoroutineStart 指的是coroutine启动的选项,总的来说有四种:

/*
 * * [DEFAULT] -- immediately schedules coroutine for execution according to its context;
 * * [LAZY] -- starts coroutine lazily, only when it is needed;
 * * [ATOMIC] -- atomically (non-cancellably) schedules coroutine for execution according to its context;
 * * [UNDISPATCHED] -- immediately executes coroutine until its first suspension point _in the current thread_.
 */

第三个就是实际的coroutine要执行的代码段了,下面我们看看具体的执行流程:

public fun <T> async(
    context: CoroutineContext = DefaultDispatcher,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T> {
    /**
   *  初始化上下文,例如名字(方便调试)
   */
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyDeferredCoroutine(newContext, block) else
        DeferredCoroutine<T>(newContext, active = true)
    //这里是DeferredCoroutine,并且不存在父任务
    coroutine.initParentJob(context[Job])
    //Kotlin的运算符重载,会转化为对应参数的invoke方法
   //https://kotlinlang.org/docs/reference/operator-overloading.html
    start(block, coroutine, coroutine)
    return coroutine
}

public fun newCoroutineContext(context: CoroutineContext): CoroutineContext {
    val debug = if (DEBUG) context + CoroutineId(COROUTINE_ID.incrementAndGet()) else context
    return if (context !== DefaultDispatcher && context[ContinuationInterceptor] == null)
        debug + DefaultDispatcher else debug
}

下面就会调用CoroutineStart中对应的invoke方法:

    public operator fun <R, T> invoke(block: suspend R.() -> T, receiver: R, completion: Continuation<T>) =
        when (this) {
            //类似java的switch语句(TABLESWITCH/lookupswitch)
            //https://kotlinlang.org/docs/reference/control-flow.html
            CoroutineStart.DEFAULT -> block.startCoroutineCancellable(receiver, completion)
            CoroutineStart.ATOMIC -> block.startCoroutine(receiver, completion)
            CoroutineStart.UNDISPATCHED -> block.startCoroutineUndispatched(receiver, completion)
            CoroutineStart.LAZY -> Unit // will start lazily
        }

接着会去调用startCoroutineCancellable方法:

internal fun <R, T> (suspend (R) -> T).startCoroutineCancellable(receiver: R, completion: Continuation<T>) =
    createCoroutineUnchecked(receiver, completion).resumeCancellable(Unit)

//
public fun <R, T> (suspend R.() -> T).createCoroutineUnchecked(
        receiver: R,
        completion: Continuation<T>
): Continuation<Unit> =
        if (this !is kotlin.coroutines.experimental.jvm.internal.CoroutineImpl)
            buildContinuationByInvokeCall(completion) {
                @Suppress("UNCHECKED_CAST")
                (this as Function2<R, Continuation<T>, Any?>).invoke(receiver, completion)
            }
        else
            //这里create方法会去创建CoroutineImpl,但是我们看到CoroutineImpl中这方法会直接抛异常
            //wtf?实际上这里传递进来的this是编译器根据async中的lambda动态生产的类的实例,因此
            //也就是说实际的调用是那个动态类
            (this.create(receiver, completion) as kotlin.coroutines.experimental.jvm.internal.CoroutineImpl).facade

上面说到编译器会生成内部类,那么我们看看这里到底有什么黑魔法,下面贴一下具体的结构
image
反编译之后,先只看create方法:

static final class CoroutineTest.launch
extends CoroutineImpl
implements Function2<CoroutineScope, Continuation<? super Unit>, Object> {
    private CoroutineScope p$;
    
    @NotNull
    public final Continuation<Unit> create(@NotNull CoroutineScope $receiver, @NotNull Continuation<? super Unit> $continuation) {
        Intrinsics.checkParameterIsNotNull((Object)$receiver, (String)"$receiver");
        Intrinsics.checkParameterIsNotNull($continuation, (String)"$continuation");
        CoroutineImpl coroutineImpl = new ;
        CoroutineScope coroutineScope = coroutineImpl.p$ = $receiver;
        return coroutineImpl;
    }
}

创建完需要的上下文之后,会去调用拿到Continuation后,就去调用resumeCancellable方法:

internal fun <R, T> (suspend (R) -> T).startCoroutineCancellable(receiver: R, completion: Continuation<T>) =
    createCoroutineUnchecked(receiver, completion).resumeCancellable(Unit)
internal fun <T> Continuation<T>.resumeCancellable(value: T) = when (this) {
    is DispatchedContinuation -> resumeCancellable(value)
    else -> resume(value)
}

    @Suppress("NOTHING_TO_INLINE") // we need it inline to save us an entry on the stack
    inline fun resumeCancellable(value: T) {
        val context = continuation.context
        if (dispatcher.isDispatchNeeded(context))
            dispatcher.dispatch(context, DispatchTask(continuation, value, exception = false, cancellable = true))
        else
            resumeUndispatched(value)
    }

image
可以看到最终就是丢到CommonPool中(ForkJoinPool),不过在那之前会包装成一个DispatchTask:

internal class DispatchTask<in T>(
    private val continuation: Continuation<T>,
    private val value: Any?, // T | Throwable
    private val exception: Boolean,
    private val cancellable: Boolean
) : Runnable {
    @Suppress("UNCHECKED_CAST")
    override fun run() {
        val context = continuation.context
        val job = if (cancellable) context[Job] else null
        withCoroutineContext(context) {
            when {
                job != null && !job.isActive -> continuation.resumeWithException(job.getCancellationException())
                exception -> continuation.resumeWithException(value as Throwable)
                else -> continuation.resume(value as T)
            }
        }
    }

    override fun toString(): String =
        "DispatchTask[$value, cancellable=$cancellable, ${continuation.toDebugString()}]"
}

在我们的场景下最终会调用:Continuation#public fun resume(value: T),这里的实际会调用:

abstract class CoroutineImpl(
        arity: Int,
        @JvmField
        protected var completion: Continuation<Any?>?
) : Lambda(arity), Continuation<Any?> {
    override fun resume(value: Any?) {
        processBareContinuationResume(completion!!) {
            doResume(value, null)
        }
    }
}
@kotlin.internal.InlineOnly
internal inline fun processBareContinuationResume(completion: Continuation<*>, block: () -> Any?) {
    try {
        val result = block()
        if (result !== COROUTINE_SUSPENDED) {
            @Suppress("UNCHECKED_CAST")
            (completion as Continuation<Any?>).resume(result)
        }
    } catch (t: Throwable) {
        completion.resumeWithException(t)
    }
}

这里doResume就是上文提到Kotlin编译器生成的内部类:

    @Nullable
    public final Object doResume(@Nullable Object var1_1, @Nullable Throwable var2_2) {
        var5_3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
        switch (var0.label) {
            case 0: {
                v0 = var2_2;
                if (v0 != null) {
                    throw v0;
                }
                var3_4 = this.p$;
                this.label = 1;
                v1 = DelayKt.delay$default((long)1000, (TimeUnit)null, (Continuation)this, (int)2, (Object)null);
                if (v1 == var5_3) {
                    return var5_3;
                }
                ** GOTO lbl18
            }
            case 1: {
                v2 = throwable;
                if (v2 != null) {
                    throw v2;
                }
                v1 = data;
lbl18: // 2 sources:
                var4_5 = "World!";
                System.out.print((Object)var4_5);
                return Unit.INSTANCE;
            }
        }
        throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
    }

可以看到实际上Kotlin使用状态机实现的Coroutine,根据label的状态决定要执行的代码块,Kotlin会在编译时根据可suspend的方法插入相应的label,从而实现主动让出线程资源,并且将本地变量保存到Continuation的实例变量中,等到下次得到调度的时候,根据label来决定要执行的代码块,等到函数实际执行完的时候则直接返回对应的返回值(没有的话则是默认值).
比如看下面这段代码:

val a = a()
val y = foo(a).await() // suspension point #1
b()
val z = bar(a, y).await() // suspension point #2
c(z)

这段代码总共有三种状态:

  1. 初始状态,在所有的suspension point点之前
  2. 第一个暂停点
  3. 第二个暂停点

每一个状态都是continuation的入口点之一,这段代码会编译为一个实现了状态机的匿名类,有一个状态变量用于保存当前状态机的状态,以及用于保存当前coroutine的本地变量,编译后的代码用Java代码类似下面:

class <anonymous_for_state_machine> extends CoroutineImpl<...> implements Continuation<Object> {
    // 状态机当前的状态
    int label = 0
    
    // coroutine的本地变量
    A a = null
    Y y = null
    
    void resume(Object data) {
        if (label == 0) goto L0
        if (label == 1) goto L1
        if (label == 2) goto L2
        else throw IllegalStateException()
        
      L0:
        a = a()
        label = 1
        data = foo(a).await(this) // 'this' 默认会被传递给await方法
        if (data == COROUTINE_SUSPENDED) return //如果需要暂停则返回
      L1:
        //重新得到调度
        y = (Y) data//保存本地变量
        b()
        label = 2
        data = bar(a, y).await(this) 
        if (data == COROUTINE_SUSPENDED) return 
      L2:
        Z z = (Z) data
        c(z)
        label = -1 // No more steps are allowed
        return
    }          
}    

当coroutine开始执行的时候,默认label为0,那么就会跳转到L0,然后执行一个耗时的业务逻辑,将label设置为1,调用await,如果coroutine的执行需要暂停的那么就返回掉。当需要继续执行的时候就再次调用resume(),这次就会跳转到L1, 执行完业务逻辑后,将label设置为2,调用await并根据是否需要暂停来return,下次的继续调度,这次会从L2开始执行,然后label设置为-1,意味着不需要执行完了,不需要再调度了。

回到最初的代码段,我们首先调用了delay方法,这个方法默认使用ScheduledExecutorService,从而将当前的coroutine上下文包装到DispatchTask,再对应的延迟时间之后再恢复执行,恢复执行之后,这时候label是1,那么就会直接进入第二段代码,输出World!

    @Test
    fun async() {
        async {
            //在另外的线程池中执行,通过保存当前的执行上下文(本地变量、状态机的状态位等),并丢到
            //ScheduledExecutorService中延迟执行
            delay(1000)
            print("World!")
        }
        //主线程中直接输出
        print("Hello ")
        Thread.sleep(2000)
    }

165021507294969_ pic_hd

总结

本文大致讲解了一些Kotlin中Coroutine的实现原理,当然对于协程,很多编程语言都有相关的实现,推荐都看一下文档,实际使用对比看看。
image

参考资料

  1. https://github.com/Kotlin/kotlin-coroutines
  2. https://en.wikipedia.org/wiki/Coroutine
  3. https://www.lua.org/pil/9.html
  4. https://golang.org/doc/effective_go.html#concurrency
  5. https://www.youtube.com/watch?v=4W3ruTWUhpw
  6. https://www.youtube.com/watch?v=EMv_8dxSqdE

Flag Counter

注册中心的设计与实现

问题

客户端如何知道某一个服务的可用节点列表?

要求

  • 每个服务的实例都会在一个特定的地址(ip:port)暴露一系列远程接口,比如HTTP/REST、RPC等
  • 服务的实例以及其地址会动态变更(虚拟机或Docker容器的ip地址都是动态分配的)

解决方案

负载均衡器

类似Nginx这类负载均衡器貌似可以解决这个问题,但是只支持静态配置,当我们对服务动态扩容、缩容时,需要联系运维进行对应的配置变更,而且如果你的服务运行在Docker或K8S时,节点的IP都是动态分配的,这时再通过Nginx去做服务发现会变的非常麻烦。另外引入一个中间层,就引入了一个潜在的故障点,虽然Nginx的性能很高,但多经过一层必然会造成一定的性能损耗。

server {
    location / {
        proxy_pass http://localhost:8080;
    }

    location /images/ {
        root /data;
    }
}

注册中心

image

在动态的环境下最好的方式是通过注册中心解决这个问题,实现一个服务注册中心,存储服务实时的地址、元数据、健康状态等信息。注册中心负责处理服务提供者的注册、注销请求,并定时对该服务的实例进行健康检查。客户端通过注册中心暴露的接口查询该服务的可用实例。

设计方案

注册中心其实本质上还是一个存储系统,最早可能就是个静态配置文件,随着系统变得越来越复杂,同时加上现在服务大多都是部署在容器之中,节点IP的变更都是动态的,导致静态配置文件的形式已经完全不可用了,因此我们就需要节点能够动态的注册、注销。那么注册中心就需要能够存储这个信息,并实时的维护更新。
最简单其实可以存储到MySQL中,由一个单机节点负责处理所有的请求,比如注册、注销、健康监测、服务状态变更事件推送等。
但由于单点问题,单机版没有很好的容灾性,那么我们可以缓存来解决,在客户端SDK中通过多级缓存机制: 内存->磁盘快照, 解决因注册中心挂掉而不能获取到数据的问题。
但这样的架构还是有问题,虽然客户端已经和注册中心解耦了,但当注册中心挂掉时,新扩容、缩容或者正常上下线的节点,由于注册中心挂掉了,服务的调用者是不能够获取到这个信息的,因此就会获取到过期的数据。我们很容易就想到冗余,多部署几个节点,但不同于业务应用,注册中心本身是有状态的,不能像业务应用一样简单的部署多个节点解决问题,我们还需要数据同步的问题。

数据同步

数据同步其实有非常多的方式,我们看一下业界一些开源注册中心的解决方案.

Eureka 1.X

image

Eureka client会优先和同一个可用区的eureka server通讯,如果由于网络问题、server挂掉等原因导致通讯异常,那么客户端fail over到其他可用区的eureka server上重试。
Eureka的多副本的一致性协议采用类似“异步多写”的AP协议,Eureka server会将收到的收到的所有请求都转发给它所知道的所有其他eureka server(如果转发失败,会在下一次心跳时继续重试),其他eureka server收到请求会,会在本地重放,从而使得不同eureka server之间的状态保持一致。从这一点也可以看出来,eureka是一个AP系统,保证最终一致,因为eureka所有server都能提供服务,并不是一个leader based的系统,当客户端从eureka server 1获取服务B的数据时,可能服务B是和eureka server 2建立的连接,而此时server 2还没有将最新数据同步到1,因此此时客户端就会获得过期的数据。
看起来一切都很好,但eureka这样类似点对点的同步算法, 会有什么问题呢?

  • 采用广播式的复制模型,所有的server会将所有的数据、心跳复制给其他所有的server,实现起来很简单,但却不失为一种不错的方案,但随着服务节点的增多,广播逐渐会成为系统的瓶颈,因为写入是不能横向扩展的,每次写入请求必须转发给其他所有的server,因此即使你扩容的更多的节点,系统的性能不但不会提升,反而会有很大的下降。
    这里再提一下eureka的其他一些问题:
  • 客户端会获取全量的服务数据,并且不支持只获取某一个单独的服务信息,导致占用客户端大量的内存,即使你可能只需要其中某一个服务的地址。
  • 只支持定时更新:eureka的客户端是通过pull的方式从server获取服务的最新状态,这样会有几个问题:
    • 获取有一定的延迟,具体取决于应用的配置。
    • 如果pull的间隔配置的很低,会导致产生很多无用的请求,比如某个节点可能一天才发布一次,但客户端可能每秒都会pull一次,导致浪费系统的资源。
    • 配置太多。首次注册延迟、缓存定期更新周期、心跳间隔、主动失效检测间隔等等,当然了也可以说是优点。

当我们扩容一个新的eureka server时,服务启动后,会优先从临近的节点中获取全量的服务数据,如果失败了会继续尝试其他所有节点,如果成功了,那么这个节点就可以开始正式对外提供服务。

Eureka 2.x

image

eureka 2.x主要就是为了解决以上几个问题而诞生的,主要包含以下几点:

  • 支持按需订阅:eureka客户端支持只订阅自己感兴趣的服务数据,eureka server将只会推送客户端感兴趣的数据。
  • 数据推送从pull改成push模式。
  • 优化同步算法。跟eureka1.x一样,eureka2.x也会将数据广播给其他节点,但与其不同的是,2.x不会将每一个服务实例的心跳也发送给其他节点,这个简单的优化大大减少了系统整体的流量,提升了系统的扩展性。
  • 读写分离。Eureka2.x将eureka集群分为了写集群和读集群,注册中心是一个典型的写少读多的系统,不管是手动扩容还是自动扩容,扩容之前都可以大概预估一下系统当前的压力,并针对性的对写、读集群扩容。
  • 审计日志以及控制台。

image

eureka2.x虽然进行了大量的优化,但其实还是有些问题,写集群仍然存储的是全量的服务数据,如果服务规模非常大的话,仍然造成瓶颈,需要考虑其他一些分片的方案。

Zookeeper

image

Zookeeper的基于ZAB协议,ZAB是一个类Paxos的分布式一致性算法,因此zk的复制其实是交由zab协议来保证的,当leader收到写请求后,会将整个请求消息复制给其他节点,其他节点收到消息后,会交由本机的状态机处理,从而实现数据的复制,

image

很多人都说ZK是一个CP系统,其实个人觉得单纯的用CAP来描述一个分布式系统已经不太准确了,比如Zookeeper, 默认情况下客户端会连接到不同的节点,
而节点之间的数据和leader是不同步的,存在一定的延迟,因此会导致读取到的数据不一致,可能存在一定的延迟,但是可以通sync调用,强制同步一把,从而实现更强的一致性。那么zk到底是个AP系统还是CP系统呢?
这里再提一下zk的扩展性,zk基于ZAB协议,写入都必须经过leader,并同步到其他follower节点,因此增加更多的写入节点,意味着写入需要同步到更多的节点,从而引起性能下降,由此也可以看出zk并不具备横向扩展性,因此如果简单的通过zk去做服务发现,随着服务规模的增长,比如会遇到瓶颈。
但我们可以换一种思路,把zk当成一个存储,基于一个CP系统构建一个AP的注册中心,相较于客户端直连zk集群,改成server与zk集群建立连接,当server收到客户端的写请求时,转换成zk对应的操作,其他server节点设置对应的watch,监听服务状态的变更,从而实现数据的同步,对于其他类似健康监测、服务状态变更事件推送等则由注册中心的server完成。
但其实选择zk最需要考虑的问题是运维,因为zk相对来说是一个非常复杂的系统,你能不能用得好、出了问题能不能hold得住,这都是一个疑问,比如zk的状态机你真的理解了么?ZAB协议知道咋回事么?临时节点知道原理么?事件推送、连接管理都有哪些坑?

image

Alibaba Nacos

image

Nacos是阿里巴巴开源的动态服务发现、配置管理和服务管理平台。对于注册中心这块来说,其一致性算法是基于Raft实现的,Raft类似Paxos,也是一种一致性协议算法,但是相对Paxos来说,要容易理解的多。类似上面说的基于zk的方案,nacos也是基于一个CP协议打造的一个AP系统,客户端本地是支持快照的,即使服务端挂掉,也不影响客户端的使用。

小结

可以看出数据同步其实有非常多的解决方案,具体如何选择其实还是要看业务场景、服务规模等,大部分情况下完全没必要自己造轮子,无脑选择nacos、eureka就可以了。

CP or AP

CAP理论指出,在分布式存储系统中,不可能同时满足以下三种条件中的两种:

  • 一致性: 每个读请求总是能够获取到最新写入的值
  • 可用性: 每个请求都能够接受到对应的响应,但不需要包含最新写入的值,也就是允许读取到过期的数据
  • 分区容忍性: 当发生了网络分区时(节点之间发生了丢包等现象造成不能正常通讯),那么系统就必须在C和A之间选择一个
数据一致性

注册中心最核心的功能其实就三个:

  • 对于调用者来说,能够根据服务的ID查询到服务的地址、元数据、健康程度等信息
  • 对于服务提供者来说,能够注册、注销自身提供的服务
  • 注册中心能够检测到服务实例的健康程度,并能够通知给客户端

我们设想一下,假如必须要满足一致性的话,那么当发生网络分区时,注册中心集群被一分为二:多数区、少数区。那么多数区因为大多数节点仍然能够选出leader,仍然能够正常处理服务实例的注册、注销、健康监测请求,分区内的客户端也能正常的获取到对应的节点。但是在少数区的节点,由于不能够组成大多数节点,因此不能正常的选举出leader,而由于我们选择了一致性,就不能处理客户端的读写请求,如果我们处理注册、注销请求的话,就必然会造成数据不一致,而如果我们处理读请求的话,那么这个时候读取的其实是过期的数据,也不能满足一致性。

image

比如说典型的ZK3地5节点部署架构,当发生网络分区时,机房1和机房2能够正常通讯,但机房3和其他两个机房发生了网络分区,由于zk的特性,只要大多数节点能够正常通讯,那么就能够保证整个zk集群正常正常对外提供服务,但是位于机房3的zk节点5由于不能和其他节点通讯,是不能够对外提供服务的,读写请求都不能够处理,对应于服务发现的场景来说,就是扩容、缩容的节点不能够正常的注册、注销,另外正常的节点心跳检测也会异常。
但我们发现,虽然机房3不能和其他两个机房正常通讯,但机房3内所有的服务是能够正常通讯的,机房内的服务调用其实是完全正常的,但由于发生了网络分区,我们优先选择了一致性,对应服务发现的场景来说,也就是服务调用者是获取不到服务实例列表的,即使是同机房内能够正常通讯的节点也不行,这样的行为对于业务方来说通常是不可接受的。

但对于服务发现的场景来说,一致性其实并没有那么重要,当发生网络分区,客户端获取到的是不完整的节点列表,比如说可能不包含部分节点(因为不能和另一个分区的leader节点通讯,新注册上来的节点也就获取不到),另外也可能包含其实已经下线的节点(因为发生了网络分区,心跳监测也会发生异常),但这个其实问题不大,客户端可以监测对应的异常,对于幂等的读取请求可以failover到其他节点上重试,对于写请求,需要对应的服务提供者处理好去重,保证幂等,客户端可以根据自己的业务场景决定具体的策略。但如果选择了一致性,客户端从注册中心获取不到节点,服务整体是不可用的。

可用性

对于服务发现的场景来说,其实大部分业务方的需求其实一个AP系统,也就是发生网络分区时,优先选择可用性,一段时间内的数据不一致其实完全在可接受的范围之内。
比如上面说的场景,当发生网络分区时,机房3的zk节点不能和其他机房的leader节点通讯,但如果我选择了A, 那么也就是说注册中心可以返回给客户端过期的数据,比如客户端A获取服务B的节点列表,注册中心可能返回了10个节点,但这个10个节点中可能就有一些节点已经下线了,因为不能够此时注册中心不能处理写请求,如果能够处理写请求的话,情况会更复杂一些,等网络分区恢复之后,我们还需要处理数据冲突的问题,另外这个10个节点的数据可能也不全,可能没有包含新扩容的节点(比如机房2扩容了5个节点,并注册到了机房1或者2的leader,但zk5以为不能和leader正常通讯,是获取不到这个数据的)。

健康检查

在微服务式的架构之下,每一个服务都会依赖大量其他的服务实例,当其中任何一个实例出现了故障时,系统必须能够在一定是时间内监测到异常,并通知给对应的调用方。大部分系统都是通过心跳机制去监测服务的健康程度。
健康检查大致分为两类:

liveness check

Liveness检查主要是用来监测服务的存活状态,例如进程是否还在、端口是否能够Ping通等,如果系统挂掉,那么这个时候监测不到进程id,注册中心会将对应的节点标为异常,并通知对应的节点。

readiness check

Readiness检查的作用通常是用来监测服务是否能够对外提供服务,比如说即使能够监测到应用的进程id,但可能应用还在启动中、缓存还没有预热、代码还没经过jit预热等。

image

探针类型

一般来说探针大致分为两种:

TCP

image

注册中心会定时尝试和对应的ip:port建立tcp连接,如果能够正常建立连接,则表明服务当前处于健康状态,否则则为异常。

HTTP

image

注册中心会定时调用对应的接口,如果状态码、header或者响应满足对应的要求,那么则认为该服务当前健康,我们可以在这个接口中针对自己的业务场景检测对应的组件,比如数据库连接是否已经建立、线程池是不是已经被打满了等。

其他

其他还有一些比如说针对数据库,可以通过发送一个sql,校验数据库是否能在一定的时间内返回结果,从而监测数据库的健康状况。

探针执行策略

当然这里还有一些其他的策略,比如超时时间、调用间隔、几次检测失败才将服务视为异常等。这方面可以参考一下Nginx:

upstream backend {
    server backend1.example.com;
    server backend2.example.com max_fails=3 fail_timeout=30s;
}

Service Mesh

image

蹭下热点简单说一下Service Mesh,service mesh的要解决的一个很重要的痛点就是多语言的问题,用java的做微服务一般来说直接用Spring Cloud这一套就可以了,限流、熔断、服务发现、负载均衡等都有对应的组件支持,如果团队中技术栈是统一的,倒是没什么问题,但是在微服务的架构下,每个团队负责维护自身的服务,这个时候你并不能确保所有的服务都是用同一个语言实现的,但限流、熔断、服务发现等特性是每个微服务都需要的特性,这个时候你就需要将eureka、Hystrix用各个不同的语言实现一次,这是一件非常复杂、繁琐且有挑战的事情,很难保证你的代码没有bug。因此就出现了Service Mesh,将一个agent/sidecar和服务部署在同一个节点,并接管服务的流量,并能够分析流量,从而得知其协议、要调用的服务等信息,并针对该服务进行服务发现、限流等措施。
那么在多语言的情况下如何去做服务发现呢?给每个语言开发一个单独的SDK? 也是一种可行的方案,但正如上文所说,非常复杂,而且工作量很大。

DNS

DNS可以说是目前应用最广泛、最通用、支持最广泛的寻址方式。所有的编程语言、平台都支持。因此使用DNS作为服务发现的方案是一个非常好的思路,这也正是K8S和Service Mesh(Istio)的寻址方案。

K8S基于DNS的寻址方案

image

K8S的基础概念这里不再累述,如图所示,我们在k8s集群中部署一个uservice,并指定3个pod(实例/节点),应用部署之后,k8s会给应用分配ClusterIP和域名,并生成一条对应的DNS记录,将域名映射到ClusterIP。

  • 当我们调用http://userservice/id/1000221时,k8s首先会进行域名解析,将useservice解析后得到该服务对应的ClusterIP。
  • 客户端向ClusterIP发起请求 ,kube-proxy拦截到请求报文,得到后端pod的IP地址列表,并根据一定的负载均衡策略,选择一个pod进行请求的处理。
Service Mesh Istio基于DNS寻址方案

image

Istio的方案其实和K8S几乎是一样的,只不过说service mesh会部署一个sidecar,而sidecar会接管应用所有的流入、流出流量,因此中间会过两层sidecar(客户端、服务器端都会部署一个sidecar)。如图所示,除了红色部分外其他步骤都是一致的。

Alibaba Nacos DNS-F

image

Nacos也支持通过dns进行服务发现,dns-f客户端和应用部署在同一节点,并拦截应用的dns查询请求:

  • 首先,应用ServiceA直接通过域名调用ServiceB的接口
  • DNS-F会拦截到ServiceA的请求,通过注册中心查询,是否拥有该服务的注册信息,
    若有则根据一定的复杂均衡策略,返回ip
  • 如果没有查询到,则交给底层的操作系统处理
小结

如果继续DNS做服务发现,那么应用就不再需要关心注册中心等细节,对调用方来说就和普通的HTTP调用一样,传入一个域名,具体的域名解析交给底层的基础设施,比如K8S、Istio等,这样的话比如Dubbo、配置中心等应用,甚至是数据库的地址,都只需要配置成一个域名,这样的话Dubbo就不再需要配置中心了,只要传入一个服务的表示com.xxxxx.UserService:version, k8s/istio会解析出最终的地址,并且能够针对应用的流量,做限流、重试、监控等,应用能够专注于业务逻辑,这些事情都不需要关心,也不用耦合在代码里,都交给底层基础设施统一管控、升级等。

总结

本文大概讲了一下注册中心的设计,其中还有非常多的组件、细节没有涉及到,比如多数据中心、服务事件通知风暴等等问题,后面有时间会继续补充。

参考资料

JVM指令集中tableswitch和lookupswitch指令的区别

编译Switch语句

编译器会使用tableswitch和lookup指令来生成Switch语句的编译代码,并且都只能支持int类型的条件值.因此byte,char,short类型的值都会被向上拓展为int类型。

TableSwitch

tableswitch的做法是直接将条件值和case进行一一比较,整个操作为O(1),因此非常快。
例如下面这段代码

int chooseNear(int i) {
    switch (i) {
        case 0:  return  0;
        case 1:  return  1;
        case 2:  return  2;
        default: return -1;
    }
}

可以发现,case分支的条件值很紧凑,中间没有断层,编译后的代码为:

tableswitch 1 3
    OneLabel
    TwoLabel
    ThreeLabel
  default: DefaultLabel

但是如果中间有断层的话,编译器会进行一些优化,看这段代码:

switch (inputValue) {
  case 1:  // ...
  case 3:  // ...
  case 4:  // ...
  case 5:  // ...
  default: // ...
}

这段代码可以说近乎是紧凑的,只有2是缺少的,编译代码如下:

tableswitch 1 6
    OneLabel
    FakeTwoLabel
    ThreeLabel
    FourLabel
    FiveLabel
  default: DefaultLabel

  ; 
  ......
  FakeTwoLabel:
  DefaultLabel:
    ; default code

可以看到编译器为‘2’这种情况,自动生成了一个虚假的case:FakeTwoLabel,而当执行到case 2的时候,会自动跳转到default的处理代码。

lookupswitch

lookupswitch是对索引表中的键进行比较,如果条件值和某个键匹配,则跳转到这个键对应的分支偏移量继续执行,若没有键值符合,那么就执行default处理代码段。而且键值必须是排序过的,这样将会比线性查找效率好很多,例如采用二分查找,效率为O(log n)。
下面这段代码,case的条件值很分散,有上百个断层,因此编译器也必须生成上百个fake case,结果会生成一个超大的table,class文件的大小爆炸式增长,这种做法显然不符合现实,因此编译器会生成一段lookupswitch指令

switch (inputValue) {
  case 1:    // ...
  case 10:   // ...
  case 100:  // ...
  case 1000: // ...
  default:   // ...
}

编译后的代码为:

lookupswitch
    1       : Label1
    10      : Label10
    100     : Label100
    1000    : Label1000
    default : DefaultLabel

这个表只有五项,4个真实的值,如果采用二分查找,log4=2,JVM至多只需要2次就可以找到结果。就算表有100项,也才比较7次左右好么。

结论

tableswitch用于case比较紧凑的代码,而lookup用于case比较分散的代码。如果不考虑空间的话,tableswitch指令比lookup指令有更高的执行效率。

Flag Counter

Cloudflare接口服务中断故障复盘与思考

前言

最近一段时间,各大厂商故障频发,就在上个月Cloudflare就出现了一次持续六个多小时的故障,接口的成功率下降至75%左右,并且管理后台已经几乎不可用,比平常慢了80多倍。Cloudflare也给出了一份详细的故障报告,
A Byzantine failure in the real world
, 简单来讲就是由于交换机异常,导致出现了网络丢包,etcd集群无法正常通讯导致无法正常对外提供服务,而上游业务强依赖etcd,导致服务出现异常。

故障原因

在一些需要强一致的场景下,Cloudflare大量使用了etcd来作为底层的存储层,这样即使其中少数节点挂掉,集群仍然能够正常对外提供服务,避免了单点问题。

首先在某一时间点,交换机出现异常(并且触发了内部的告警,无法通过ping连接到交换机),此时并没有完全挂掉,而只是出现部分节点网络丢包,因此没有触发自动切换机制(交换机有2个节点互备,非单点)。六分钟后,交换机在没有人为干预的情况下自动恢复了,但这几分钟的网络异常却导致了更严重的问题。异常交换机所在的机架上部署了etcd集群其中一个节点。而在交换机出现异常一分钟后,etcd集群节点之间通讯异常,导致无法选举出一个稳定的leader,集群无法正常对外提供读写服务:

4685_0

  • 节点1(部署在受影响机架上的节点)和节点3(当前leader)发生网络丢包
  • 节点1和节点2之前的网络通讯正常
  • 节点2和节点3之间的网络正常

由于节点1无法与当前leader节点3正常进行通讯,因此当选举超时后,在节点1的视角下,当前leader已经挂掉,因此会转到candidate,增加自己的term并尝试发起选举,节点2收到投票后,会更新自己的term,然后告诉节点3你已经不是leader了。但由于节点3无法和节点1进行正常通讯,因此超时后节点3会重复刚刚的动作,增加自己的term并尝试发起选举。整个集群选举无法出一个稳定的leader,导致无法正常对外提供读写服务。

这里只是举例其中一种可能发生的情况,也有可能节点1的日志落后于其他两个节点,因此节点2会拒绝节点1的选举请求,会投票给节点3,因此节点3仍然当选为leader,但是由于节点1无法与节点3(leader)正常通讯,因此超时后又会重复发起投票,而选举期间写入会阻塞,进而影响对外服务。

我们记得在Raft原始的论文中提到过:

[Consensus algorithms] are fully functional (available) as long as any majority of the servers are operational and can communicate with each other and with clients.

也就是说即使发生了网络丢包等异常情况,只要大多数节点仍然能够正常通讯,Raft协议仍然能够保证正常提供服务,但通过上面的描述,我们发现其实并不是如此,节点1和节点2/3发生了网络分区,导致整个集群都不可用。 那是不是说Raft无法应对这种情况呢,其实不是的。
通过上面的分析,我们可以看出,节点2和节点3(leader)是能够正常通讯的,但是节点1和leader由于网络丢包无法建立连接,因此发起了选举,节点2和节点1不停的发起去选举/投票/选主,导致集群无法正常提供服务,其实Diego Ongaro’s thesis中提到过解决方案,即PreVote,也就说当节点1作为候选者想要发起选举之前,首先需要发起一次PreVote预投票,如果得到了大多数节点的同意,此时才会增加自身的term并发起投票, 这里有一个比较关键的地方是,follower只有在当前leader超时后(在electionTimeout内仍然没有收到leader的心跳包),才会投票给候选者,这样节点1发起选举时,节点2由于能够和当前leader节点3正常通讯,因此会拒绝节点1的预投票prevote,从而避免了上述问题。

image

只要是分布式系统,就必然会遇到网络分区/丢消息的问题,例如Zookeeper/TiDB/HBase/MongoDB等,Raft协议提供了prevote的解决方案,但是相对来讲还是有一定的复杂性以及侵入性,并且需要上层应用打开相关配置,其实除了协议层的解决方案之外,还有另外一种更加通用的方式,在网络层解决,感兴趣的可以阅读这篇论文Toward a Generic Fault Tolerance Technique for Partial Network Partitioning, 简单来讲,比如节点1和节点4之前发生了网络分区无法正常通讯,但是从1 -> 2 -> 3 -> 4的网络是正常的,此时我们可以通过修改路由,做一次中转,将节点1到节点4的数据包通过节点2和节点3做一次转发,从而实现网络分区对上层无感知,当然性能还是有所损失。

image

在本次故障报告中,Cloudflare将其描述为拜占庭将军问题,其实不是太准确,说的直白一点,其实就是丢消息,并不是拜占庭将军问题,也不需要**BFT(byzantine fault tolerance)**协议才能解决,其实etcd本身已经支持了prevote,只是老版本默认是关闭的而已。

对业务的影响

Cloudflare的控制面板服务底层依赖的关系数据库部署在了同一个可用区的不同集群,每个集群都包含了一个主库、实时同步的备库以及一个或者多个异步备份节点。这样即使同一个数据中心的数据库挂掉,仍然能够切换到其他节点,而对于跨数据中心的冗余来说,Cloudflare将数据备份到了不同地理位置的多个数据中心,并利用etcd来进行集群成员的发现以及协调等。

在故障期间,etcd由于无法选举出稳定的leader,对外无法提供写入服务,因此两个集群之前的健康检测机制出现异常,无法正常交换对应集群内主库是否健康的消息,从而触发了自动的主从切换。

但是集群管理系统存在一个问题,当主从切换时,需要重建所有的备份,因此虽然主库已经恢复,但由于需要重建备库,此时备库集群是不可用的,具体恢复时间取决于主库的数据大小。其中一个数据库集群很快恢复了,因此没有造成太大的影响。但是另外一个集群,由于API的权限认证以及控制面板依赖,因此会又大量的读请求进来,Cloudflare通过多个备库来实现读写分离,降低主库的压力,但当发生主从切换时,需要重建所有备库,导致切换完成后备库全都不可用,所有的流量都打到了主库,造成主库负载非常高,故障主要是因为该问题引起

减轻主库的负载

由于所有的流量都打到了主库,因此此时主库负载非常高,首先是限流,其次上文中提到过,每个数据库集群在其他数据中心都有备份,但是自动切换机制仍然不支持,因此通过手动切换的方式,将流量切换到了其他数据中心,这一步极大的提升了接口的可用性。但是控制台由于涉及到用户登录等操作,创建session等流程需要写数据库以及redis集群,此时控制面板的体验变得更差了。

六小时后,备库重建完成,服务开始陆续恢复,并关闭对应的降级措施。

总结

当我们在做服务的稳定性时,总是会梳理系统有没有单点,比如数据库/Redis/第三方服务等,在这次故障中我们发现Cloudflare做的已经相当完善了,比如交换机是主从互备的,数据库采用了etcd,并且etcd基于Raft协议实现,只要大多数节点能够正常通讯,服务都是可用的,同时数据还备份到了不同的数据中心,故障过程中读流量支持切换到其他数据中心,而大多数其他小公司可能只部署到了一个数据中心,如果遇到同样的问题,由于所有的读流量都打到了主库,可能会导致主库挂掉等,问题会严重的多。在复盘的过程中,我们发现其实只是做到无单点/冗余,很多时候并不够,在本次故障中,大多数服务并没有完全挂掉,只是部分服务/流量异常,每个组件都处于降级状态,导致发生蝴蝶效应:

  • 交换机只是到部分节点的数据丢包,因此没有触发自动切换
  • etcd集群虽然是分布式的,但是由于发生了网络分区,导致集群无法选举出稳定的leader,无法对外提供写入服务
  • 网络恢复后,数据库需要重建备份,导致流量全部打到主库,主库负载太高,需要限流
  • 流量切换到其他数据中心后,接口开始恢复,但是由于无法写入,对于登录等涉及到写入的操作,情况反而恶化了

思考

  • 当我们梳理系统的依赖时,总是会去看是否为单点,比如Redis Sentinel/Redis Cluster,MySQL是否为主备,etcd是否为集群部署,但其实很多时候,挂掉并不可怕,可怕的是慢了,或者部分挂掉,比如etcd仍然能够提供读取服务,但是写入已经挂掉了,比如主从切换后由于需要重建所有备库,所有流量都打到主库造成负载过高。
  • 服务限流及隔离/依赖梳理,比如本次故障中,读取的流量可以切换到其他数据中心,降低主库的压力,而登录等涉及到写入的操作仍然在主库执行,而此时主库由于在进行重建备库等操作,性能会有所下降,因此上游服务仍然需要限流/降级措施,否则主库流量过大挂掉的话,问题会更严重
  • 故障演练的重要性,虽然数据库支持自动切换,但是没想到切换后重建备库花了六个多小时,之前AWS的S3也出过一次严重的故障,索引服务挂掉,重启时需要重建索引,但是由于数据量实在太大,重建花了很长时间。稳定性/高可用说起来很简单,很多时候系统没有出问题,不是因为系统稳定性做的有多完善,而是因为还没出问题。一旦下游出现了一丝抖动,可能会引起非常严重的故障,类似Netflix Facebook等公司的chaos monkey,将故障演练变为一种日常,例如随机关掉一台机器/随机网络丢包等。
  • 国外公司个人觉得做的很好的一点是,会非常大方的承认自己的过错,对外公布故障的影响面/前因后果以及后续的action等,而国内这方面还差的很远
  • 简单话/流程化/自动化,涉及到人工介入的,即使有一键降级等措施,从收到告警/登录VPN/定位/解决也需要几分钟
  • 对于底层的系统组件要了解其原理/协议,虽然etcd是分布式数据库,即使大多数节点挂掉仍然能够正常提供服务,但如果不了解相应的配置,仍然会吃大亏

Running Spring Boot in a Docker container

日常的Rest服务开发我都会首选SpringBoot,因为它本身的易用性以及自带的各种方便功能、生态等,今天就简单讲一下如何将Spring Boot应用跑在Docker容器中

项目搭建

首先打开Idea,选择初始化一个Spring Boot应用,然后一路回车下去,待Idea下载完依赖,开始编码
image
这里写一个简单的接口:

@RestController
@SpringBootApplication
public class DemoApplication {

	@GetMapping("/hello")
	public String hello() {
		return "Hello World";
	}

	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}
}

然后运行下面的命令验证服务是否正常:

mvn clean package  -Dmaven.test.skip=true 
java -jar target/demo-0.0.1-SNAPSHOT.jar

容器化

下面我们就开始容器化这个简单的Spring Boot应用

创建Dockerfile文件

首先在项目的根目录创建一个Dockerfile文件,主要不要搞成驼峰命名:

From java:8


VOLUME /tmp

#将打包好后的Jar文件放到image中
Add target/demo-0.0.1-SNAPSHOT.jar  app.jar
# change file access and modification times
RUN bash -c 'touch /app.jar'

EXPOSE 8080
#容器启动的时候运行Jar文件
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

构建镜像

接下来就去构建Docker镜像,不过通常这一步都会用Jenkins的去做:

docker build -t demo8  .    

如果看到下面的输出说明执行成功了:

Sending build context to Docker daemon  14.84MB
Step 1/6 : FROM java:8
 ---> d23bdf5b1b1b
Step 2/6 : VOLUME /tmp
 ---> Using cache
 ---> 91086d8b7c77
Step 3/6 : ADD target/demo-0.0.1-SNAPSHOT.jar app.jar
 ---> d161bed06e8b
Step 4/6 : RUN bash -c 'touch /app.jar'
 ---> Running in 9fbaff628989
 ---> 1fc0498bbb06
Removing intermediate container 9fbaff628989
Step 5/6 : EXPOSE 8080
 ---> Running in a5c44244b267
 ---> 3b5150c5bdd0
Removing intermediate container a5c44244b267
Step 6/6 : ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom -jar /app.jar
 ---> Running in b7a3baac9d47
 ---> 23ef7cc5e1b0
Removing intermediate container b7a3baac9d47
Successfully built 23ef7cc5e1b0
Successfully tagged demo8:latest

运行镜像

到这一步构建完成后,我们就顺利的开始运行:

docker run -d  -p 4000:8080 demo8    

然后curl一下刚才的接口看看是否正常:

± % curl localhost:4000/hello                                                                                                                                                                                                                                 
Hello World%                                                                                                                                                                                                                                                         

我们看到这里是将4000端口映射到了容器中的8080端口,我们进入容器看一下验证一下:

± % docker ps                                                                                                                                                                                                                                                            !10172
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
5f1b7f29a0b6        988ed6f466b5        "java -Djava.secur..."   29 minutes ago      Up 29 minutes       0.0.0.0:4000->8080/tcp   practical_jones
± % docker exec -it 5f1b7f29a0b6 /bin/bash                                                                                                                                                                                                                               
root@5f1b7f29a0b6:/ curl  localhost:8080/hello
Hello World

总结

SpringBoot打包后直接生成一个可执行的JAR包,天然就非常适合搭配Docker一起使用,正如本文演示的一样非常简单,下篇博客有空介绍一下如何整合K8S

Flag Counter

Java线程那点事儿

引言

说到Thread大家都很熟悉,我们平常写并发代码的时候都会接触到,那么我们来看看下面这段代码是如何初始化以及执行的呢?

public class ThreadDemo {

	public static void main(String[] args) {
		new Thread().start();
	}
}

初始化流程

代码就一行很简单,那么这行简单的代码背后做了那些事情呢?

初始化Thread这个类

首先JVM会去加载Thread的字节码,初始化这个类,这里即调用下面这段代码:

public class Thread implements Runnable {
    /* Make sure registerNatives is the first thing <clinit> does. */
    private static native void registerNatives();
    static {
        registerNatives();
    }
}

是个native方法,那么我们去看看内部实现是什么,具体的目录是openjdk/jdk/src/share/native/java/lang/Thread.c, 下载地址

#define ARRAY_LENGTH(a) (sizeof(a)/sizeof(a[0]))

//JVM前缀开头的方法,具体实现在JVM中
static JNINativeMethod methods[] = {
    {"start0",           "()V",        (void *)&JVM_StartThread},
    {"stop0",            "(" OBJ ")V", (void *)&JVM_StopThread},
    {"isAlive",          "()Z",        (void *)&JVM_IsThreadAlive},
    {"suspend0",         "()V",        (void *)&JVM_SuspendThread},
    {"resume0",          "()V",        (void *)&JVM_ResumeThread},
    {"setPriority0",     "(I)V",       (void *)&JVM_SetThreadPriority},
    {"yield",            "()V",        (void *)&JVM_Yield},
    {"sleep",            "(J)V",       (void *)&JVM_Sleep},
    {"currentThread",    "()" THD,     (void *)&JVM_CurrentThread},
    {"countStackFrames", "()I",        (void *)&JVM_CountStackFrames},
    {"interrupt0",       "()V",        (void *)&JVM_Interrupt},
    {"isInterrupted",    "(Z)Z",       (void *)&JVM_IsInterrupted},
    {"holdsLock",        "(" OBJ ")Z", (void *)&JVM_HoldsLock},
    {"getThreads",        "()[" THD,   (void *)&JVM_GetAllThreads},
    {"dumpThreads",      "([" THD ")[[" STE, (void *)&JVM_DumpThreads},
    {"setNativeName",    "(" STR ")V", (void *)&JVM_SetNativeThreadName},
};

#undef THD
#undef OBJ
#undef STE
#undef STR

//jclass cls即为java.lang.Thread
JNIEXPORT void JNICALL
Java_java_lang_Thread_registerNatives(JNIEnv *env, jclass cls)
{
    (*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods));
}

可以发现具体的实现都是由这些JVM开头的方法决定的,而这几个方法的具体实现都在hotspot\src\share\vm\prims\jvm.cpp文件中,而RegisterNatives我目前的理解其实类似一个方法表,从Java方法到native方法的一个映射,具体的原理后面再研究。

初始化Thread对象

其实就是一些赋值,名字、线程ID这些,这两个变量都是static,用synchronized修饰,保证线程安全性。

    public Thread() {
        //nextThreadNum就是变量的自增,用synchronized修饰保证可见性
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }
    
        private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null);
    }
    
        private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;

        Thread parent = currentThread();
        // 安全相关的一坨东西....

        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }
    
    
    private static synchronized long nextThreadID() {
        return ++threadSeqNumber;
    }

创建并启动线程

    public synchronized void start() {
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

这里start0()是个native方法,对应jvm.cpp中的JVM_StartThread,我们看到很多方法都是JVM_ENTRY开头,JVM_END结尾,类似于{}的作用,这里是将很多公共的操作封装到了JVM_ENTRY里面.

JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
  JVMWrapper("JVM_StartThread");
  JavaThread *native_thread = NULL;

  // We cannot hold the Threads_lock when we throw an exception,
  // due to rank ordering issues. Example:  we might need to grab the
  // Heap_lock while we construct the exception.
  bool throw_illegal_thread_state = false;

  // We must release the Threads_lock before we can post a jvmti event
  // in Thread::start.
  {
    // 加锁
    MutexLocker mu(Threads_lock);
    
    // 自从JDK 5之后 java.lang.Thread#threadStatus可以用来阻止重启一个已经启动
    // 的线程,所以这里的JavaThread通常为空。然而对于一个和JNI关联的线程来说,在线程
    // 被创建和更新他的threadStatus之前会有一个小窗口,因此必须检查这种情况
    if (java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)) != NULL) {
      throw_illegal_thread_state = true;
    } else {
      // We could also check the stillborn flag to see if this thread was already stopped, but
      // for historical reasons we let the thread detect that itself when it starts running

      jlong size =
             java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));
      // 为C++线程结构体分配内存并创建native线程。从java取出的stack size是有符号的,因此这里
      // 需要进行一次转换,避免传入负数导致创建一个非常大的栈。
      size_t sz = size > 0 ? (size_t) size : 0;
      native_thread = new JavaThread(&thread_entry, sz);

      // At this point it may be possible that no osthread was created for the
      // JavaThread due to lack of memory. Check for this situation and throw
      // an exception if necessary. Eventually we may want to change this so
      // that we only grab the lock if the thread was created successfully -
      // then we can also do this check and throw the exception in the
      // JavaThread constructor.
      if (native_thread->osthread() != NULL) {
        // Note: the current thread is not being used within "prepare".
        native_thread->prepare(jthread);
      }
    }
  }

  if (throw_illegal_thread_state) {
    THROW(vmSymbols::java_lang_IllegalThreadStateException());
  }

  assert(native_thread != NULL, "Starting null thread?");

  if (native_thread->osthread() == NULL) {
    // No one should hold a reference to the 'native_thread'.
    delete native_thread;
    if (JvmtiExport::should_post_resource_exhausted()) {
      JvmtiExport::post_resource_exhausted(
        JVMTI_RESOURCE_EXHAUSTED_OOM_ERROR | JVMTI_RESOURCE_EXHAUSTED_THREADS,
        "unable to create new native thread");
    }
    THROW_MSG(vmSymbols::java_lang_OutOfMemoryError(),
              "unable to create new native thread");
  }

  Thread::start(native_thread);

JVM_END

基本上这里就是先加锁,做些检查,然后创建JavaThread,如果创建成功的话会调用prepare(),然后是一些异常处理,没有异常的话最后会启动线程,那么下面我们先来看看JavaThread是如何被创建的。

JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) :
  Thread()
#if INCLUDE_ALL_GCS
  , _satb_mark_queue(&_satb_mark_queue_set),
  _dirty_card_queue(&_dirty_card_queue_set)
#endif // INCLUDE_ALL_GCS
{
  if (TraceThreadEvents) {
    tty->print_cr("creating thread %p", this);
  }
  initialize();//这个方法其实就是一堆变量的初始化,不是Null就是0.
  _jni_attach_state = _not_attaching_via_jni;
  set_entry_point(entry_point);
  // Create the native thread itself.
  // %note runtime_23
  os::ThreadType thr_type = os::java_thread;
  // 根据传进来的entry_point判断要创建的线程的类型。
  thr_type = entry_point == &compiler_thread_entry ? os::compiler_thread :
                                                     os::java_thread;
  os::create_thread(this, thr_type, stack_sz);
  // _osthread可能是Null,因此我们耗尽了内存(太多的活跃线程)。我们需要抛出OOM,然而不能在这做,因为调用者可能
  // 还持有锁,而所有的锁都必须在抛出异常之前被释放。
  // 代码执行到这,线程还是suspended状态,因为线程必须被创建者直接启动。
}

void JavaThread::initialize() {
  // Initialize fields
  // ...
  set_thread_state(_thread_new); // 线程的初始状态
 // ...

}

JavaThreadState记录了线程记录了线程正在执行的代码在哪一部分,这个信息可能会被安全点使用到(GC),最核心的有四种:

  1. _thread_new 刚开始启动,但还没执行初始化代码,更可能还在OS初始化的层面
  2. _thread_in_native 在native代码中
  3. _thread_in_vm 在vm中执行
  4. _thread_in_Java 执行在解释或者编译后的Java代码中

每个状态都会对应一个中间的转换状态,这些额外的中间状态使得安全点的代码能够更快的处理某一线程状态而不用挂起线程。

enum JavaThreadState {
  _thread_uninitialized     =  0, // should never happen (missing initialization)
  _thread_new               =  2, // just starting up, i.e., in process of being initialized
  _thread_new_trans         =  3, // corresponding transition state (not used, included for completness)
  _thread_in_native         =  4, // running in native code
  _thread_in_native_trans   =  5, // corresponding transition state
  _thread_in_vm             =  6, // running in VM
  _thread_in_vm_trans       =  7, // corresponding transition state
  _thread_in_Java           =  8, // running in Java or in stub code
  _thread_in_Java_trans     =  9, // corresponding transition state (not used, included for completness)
  _thread_blocked           = 10, // blocked in vm
  _thread_blocked_trans     = 11, // corresponding transition state
  _thread_max_state         = 12  // maximum thread state+1 - used for statistics allocation
};

我们看到 os::create_thread(this, thr_type, stack_sz);这行代码会去实际的创建线程,首先我们知道Java宣传的是一次编译,到处运行,那么究竟是怎么做到在不同的CPU、操作系统上还能够保持良好的可移植性呢?

// 平台相关的东东
#ifdef TARGET_OS_FAMILY_linux
# include "os_linux.hpp"
# include "os_posix.hpp"
#endif
#ifdef TARGET_OS_FAMILY_solaris
# include "os_solaris.hpp"
# include "os_posix.hpp"
#endif
#ifdef TARGET_OS_FAMILY_windows
# include "os_windows.hpp"
#endif
#ifdef TARGET_OS_FAMILY_aix
# include "os_aix.hpp"
# include "os_posix.hpp"
#endif
#ifdef TARGET_OS_FAMILY_bsd
# include "os_posix.hpp"
# include "os_bsd.hpp"
#endif
#ifdef TARGET_OS_ARCH_linux_x86
# include "os_linux_x86.hpp"
#endif
#ifdef TARGET_OS_ARCH_linux_sparc
# include "os_linux_sparc.hpp"
#endif
#ifdef TARGET_OS_ARCH_linux_zero
# include "os_linux_zero.hpp"
#endif
#ifdef TARGET_OS_ARCH_solaris_x86
# include "os_solaris_x86.hpp"
#endif
#ifdef TARGET_OS_ARCH_solaris_sparc
# include "os_solaris_sparc.hpp"
#endif
#ifdef TARGET_OS_ARCH_windows_x86
# include "os_windows_x86.hpp"
#endif
#ifdef TARGET_OS_ARCH_linux_arm
# include "os_linux_arm.hpp"
#endif
#ifdef TARGET_OS_ARCH_linux_ppc
# include "os_linux_ppc.hpp"
#endif
#ifdef TARGET_OS_ARCH_aix_ppc
# include "os_aix_ppc.hpp"
#endif
#ifdef TARGET_OS_ARCH_bsd_x86
# include "os_bsd_x86.hpp"
#endif
#ifdef TARGET_OS_ARCH_bsd_zero
# include "os_bsd_zero.hpp"
#endif

我们看到os.hpp中有这样一段代码,能够根据不同的操作系统选择include不同的头文件,从而将平台相关的逻辑封装到对应的库文件中,我们这里以linux为例,create_thread最终会调用os_linux.cpp中的create_thread方法。

bool os::create_thread(Thread* thread, ThreadType thr_type, size_t stack_size) {
  assert(thread->osthread() == NULL, "caller responsible");

  // Allocate the OSThread object
  OSThread* osthread = new OSThread(NULL, NULL);
  if (osthread == NULL) {
    return false;
  }

  // set the correct thread state
  osthread->set_thread_type(thr_type);

  // 初始状态为ALLOCATED,而不是INITIALIZED
  osthread->set_state(ALLOCATED);

  thread->set_osthread(osthread);

  // init thread attributes
  pthread_attr_t attr;
  pthread_attr_init(&attr);
  pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

  // stack size
  if (os::Linux::supports_variable_stack_size()) {
    // 如果上层未传递则计算stack_size
    if (stack_size == 0) {
      //如果为compiler_thread,则分配4M,否则默认会分配1M
      stack_size = os::Linux::default_stack_size(thr_type);

      switch (thr_type) {
      case os::java_thread:
        // Java线程用ThreadStackSize,这个值可以通过-Xss指定
        assert (JavaThread::stack_size_at_create() > 0, "this should be set");
        stack_size = JavaThread::stack_size_at_create();
        break;
      case os::compiler_thread:
        if (CompilerThreadStackSize > 0) {
          stack_size = (size_t)(CompilerThreadStackSize * K);
          break;
        } // else fall through:
          // use VMThreadStackSize if CompilerThreadStackSize is not defined
      case os::vm_thread:
      case os::pgc_thread:
      case os::cgc_thread:
      case os::watcher_thread:
        if (VMThreadStackSize > 0) stack_size = (size_t)(VMThreadStackSize * K);
        break;
      }
    }
        
    // 用两者较大的那个,min_stack_allowed默认为128K
    stack_size = MAX2(stack_size, os::Linux::min_stack_allowed);
    pthread_attr_setstacksize(&attr, stack_size);
  } else {
    // let pthread_create() pick the default value.
  }

  // glibc guard page
  pthread_attr_setguardsize(&attr, os::Linux::default_guard_size(thr_type));

  ThreadState state;

  {
    // 检查是否需要加锁
    bool lock = os::Linux::is_LinuxThreads() && !os::Linux::is_floating_stack();
    if (lock) {
      os::Linux::createThread_lock()->lock_without_safepoint_check();
    }

    pthread_t tid;
    // Linux用于创建线程的函数,这个线程通过执行java_start来启动,其中thread是作为java_start的参数传递进来的
    // 具体可见手册:http://man7.org/linux/man-pages/man3/pthread_create.3.html
    int ret = pthread_create(&tid, &attr, (void* (*)(void*)) java_start, thread);

    pthread_attr_destroy(&attr);

    if (ret != 0) {
      // 创建失败,将_osthread置为空,还记得在jvm.cpp的JVM_StartThread中会根据_osthread是否为空来判断
      // 是否创建成功
      if (PrintMiscellaneous && (Verbose || WizardMode)) {
        perror("pthread_create()");
      }
      // 清理资源,并解锁
      thread->set_osthread(NULL);
      delete osthread;
      if (lock) os::Linux::createThread_lock()->unlock();
      return false;
    }

    // 创建成功会将底层线程的ID保存在tid中
    osthread->set_pthread_id(tid);

    // 等待子线程创建完成或者终止
    {
      Monitor* sync_with_child = osthread->startThread_lock();
      MutexLockerEx ml(sync_with_child, Mutex::_no_safepoint_check_flag);
      while ((state = osthread->get_state()) == ALLOCATED) {
        sync_with_child->wait(Mutex::_no_safepoint_check_flag);
      }
    }

    if (lock) {
      os::Linux::createThread_lock()->unlock();
    }
  }

  // 线程的数目达到极限了
  if (state == ZOMBIE) {
      thread->set_osthread(NULL);
      delete osthread;
      return false;
  }

  // The thread is returned suspended (in state INITIALIZED),
  // and is started higher up in the call chain
  assert(state == INITIALIZED, "race condition");
  return true;
}

下面我们来看看pthread_create会执行的回调函数java_start,这个方法是所有新创建的线程必走的流程。

static void *java_start(Thread *thread) {
  // 尝试随机化热栈帧高速缓存行的索引,这有助于优化拥有相同栈帧线程去互相驱逐彼此的缓存行时,线程
  // 可以是同一个JVM实例或者不同的JVM实例,这尤其有助于拥有超线程技术的处理器。
  static int counter = 0;
  int pid = os::current_process_id();
  alloca(((pid ^ counter++) & 7) * 128);

  ThreadLocalStorage::set_thread(thread);

  OSThread* osthread = thread->osthread();
  Monitor* sync = osthread->startThread_lock();

  // non floating stack LinuxThreads needs extra check, see above
  if (!_thread_safety_check(thread)) {
    // notify parent thread
    MutexLockerEx ml(sync, Mutex::_no_safepoint_check_flag);
    osthread->set_state(ZOMBIE);
    sync->notify_all();
    return NULL;
  }

  // thread_id is kernel thread id (similar to Solaris LWP id)
  osthread->set_thread_id(os::Linux::gettid());

  if (UseNUMA) {
    int lgrp_id = os::numa_get_group_id();
    if (lgrp_id != -1) {
      thread->set_lgrp_id(lgrp_id);
    }
  }
  // initialize signal mask for this thread
  os::Linux::hotspot_sigmask(thread);

  // initialize floating point control register
  os::Linux::init_thread_fpu_state();

  // handshaking with parent thread
  {
    MutexLockerEx ml(sync, Mutex::_no_safepoint_check_flag);

    // 设置为已经初始化完成,并notify父线程
    osthread->set_state(INITIALIZED);
    sync->notify_all();

    // wait until os::start_thread()
    while (osthread->get_state() == INITIALIZED) {
      sync->wait(Mutex::_no_safepoint_check_flag);
    }
  }
    
    
  //这里上层传递过来的是JavaThread,因此会调用JavaThread#run()方法  
  thread->run();

  return 0;
}


void JavaThread::run() {
  // 初始化本地线程分配缓存(TLAB)相关的属性
  this->initialize_tlab();

  // used to test validitity of stack trace backs
  this->record_base_of_stack_pointer();

  // Record real stack base and size.
  this->record_stack_base_and_size();

  // Initialize thread local storage; set before calling MutexLocker
  this->initialize_thread_local_storage();

  this->create_stack_guard_pages();

  this->cache_global_variables();

  // 将线程的状态更改为_thread_in_vm,线程已经可以被VM中的安全点相关的代码处理了,也就是说必须
  // JVM如果线程在执行native里面的代码,是搞不了安全点的,待确认
  ThreadStateTransition::transition_and_fence(this, _thread_new, _thread_in_vm);

  assert(JavaThread::current() == this, "sanity check");
  assert(!Thread::current()->owns_locks(), "sanity check");

  DTRACE_THREAD_PROBE(start, this);

  // This operation might block. We call that after all safepoint checks for a new thread has
  // been completed.
  this->set_active_handles(JNIHandleBlock::allocate_block());

  if (JvmtiExport::should_post_thread_life()) {
    JvmtiExport::post_thread_start(this);
  }

  EventThreadStart event;
  if (event.should_commit()) {
     event.set_javalangthread(java_lang_Thread::thread_id(this->threadObj()));
     event.commit();
  }

  // We call another function to do the rest so we are sure that the stack addresses used
  // from there will be lower than the stack base just computed
  thread_main_inner();

  // Note, thread is no longer valid at this point!
}

void JavaThread::thread_main_inner() {
  assert(JavaThread::current() == this, "sanity check");
  assert(this->threadObj() != NULL, "just checking");

  // Execute thread entry point unless this thread has a pending exception
  // or has been stopped before starting.
  // Note: Due to JVM_StopThread we can have pending exceptions already!
  if (!this->has_pending_exception() &&
      !java_lang_Thread::is_stillborn(this->threadObj())) {
    {
      ResourceMark rm(this);
      this->set_native_thread_name(this->get_thread_name());
    }
    HandleMark hm(this);
    // 这个entry_point就是JVM_StartThread中传递过来的那个,也就是thread_entry
    this->entry_point()(this, this);
  }

  DTRACE_THREAD_PROBE(stop, this);

  this->exit(false);
  delete this;
}

我们最后再看thread_entry的代码

static void thread_entry(JavaThread* thread, TRAPS) {
  HandleMark hm(THREAD);
  Handle obj(THREAD, thread->threadObj());
  JavaValue result(T_VOID);
  JavaCalls::call_virtual(&result,
                          obj,
                          KlassHandle(THREAD, SystemDictionary::Thread_klass()),
                          vmSymbols::run_method_name(),
                          vmSymbols::void_method_signature(),
                          THREAD);
}

vmSymbols,这个是JVM对于那些需要特殊处理的类、方法等的声明,我的理解就是一个方法表,根据下面这行代码可以看出来,其实调用的就是run()方法.

  /* common method and field names */                                                            
  template(run_method_name,                           "run")                                  

然后我们回到JVM_StartThread方法中,这里会接着调用prepare()方法,设置线程优先级(将Java中的优先级映射到os中),然后添加到线程队列中去.最后会调用Thread::start(native_thread);
启动线程。

void Thread::start(Thread* thread) {
  trace("start", thread);
  // start和resume不一样,start被synchronized修饰
  if (!DisableStartThread) {
    if (thread->is_Java_thread()) {
     // 在启动线程之前初始化线程的状态为RUNNABLE,为啥不能在之后设置呢?因为启动之后可能是
     //  等待或者睡眠等其他状态,具体是什么我们不知道
      java_lang_Thread::set_thread_status(((JavaThread*)thread)->threadObj(),
                                          java_lang_Thread::RUNNABLE);
    }
    os::start_thread(thread);
  }
}

总结

  1. 一个Java线程对应一个JavaThread->OSThread -> Native Thread
  2. 在调用java.lang.Thread#start()方法之前不会启动线程,仅仅调用run()方法只是会在当前线程运行而已
  3. //todo

Flag Counter

CopyOnWriteArrayList内部工作原理剖析

CopyOnWriteArrayList是由Doug Lea在JDK1.5引入的一个并发工具类,CopyOnWriteArrayList其实线程安全的ArrayList,但又有点不一样 和HashMap和ConcurrentHashMap的关系有点类似。所有的修改操作(add/set等)都会将底层依赖的数组拷贝一份并在其之上修改,但是我们知道数组的拷贝是一个比较耗时的操作,因此通常用于读多写少的场景下,例如随机访问、遍历等。

工作原理

首先CopyOnWriteArrayList有哪些重要的域, 首先有个可重入锁用于修改(add/set等)时保证其线程安全型,另外有一个array数组用于存储实际的数据,并用volatile修饰,保证可见性。

    final transient ReentrantLock lock = new ReentrantLock();
    private transient volatile Object[] array;

    final Object[] getArray() {
        return array;
    }

    final void setArray(Object[] a) {
        array = a;
    }

ADD()工作机制

如果看过ArrayList的代码,会发现CopyOnWriteArrayList的会简单很多。

    /**
     * Creates an empty list.
     */
    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }


    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

我们会发现CopyOnWriteArrayList默认会初始化一个空数组,而在add()方法中也没有想ArrayList一样去判断当前数组的容量并去扩容(比如ensureCapacity),添加元素到数组的基本步骤:

  • 会首先尝试去加锁

  • 会调用getArray()方法获取当前数组的引用并保存到一个本地变量中,采用这种方法,一方面可以去掉一次GETFIELD调用,另外相当于保存了当前引用的快照,这样就算有其他线程并发修改引用,但是至少保证本次方法执行的一致性,当然这里直接加锁保证了不会有并发修改,因此没有这个问题。

  • 将当前数组的内容复制到新数组中,新数组的大小是老数组的长度+1,因此每次新增操作都会导致CopyOnWriteArrayList的长度自增。

  • 拷贝完成后将元素添加到新数组中。

  • 用新数组替换当前数组,用volatile修饰保证后续对其他线程可见性

其他所有的修改方法也一样,都采用了相同的加锁机制:

    public E remove(int index) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            E oldValue = get(elements, index);
            int numMoved = len - index - 1;
            if (numMoved == 0)
                setArray(Arrays.copyOf(elements, len - 1));
            else {
                Object[] newElements = new Object[len - 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index + 1, newElements, index,
                                 numMoved);
                setArray(newElements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }

读取

读取相对来说会简单很多,直接采用数组下标访问即可,但是这里读取并没有加锁,因此对于读取操作来说可能会存在延迟,读取不到最新的数据,这里读取通过getArray()方法获取的相当于是一个快照,在修改才做完成前,我们读取的都是这个快照数组的内容,对于遍历也是类似,其内部会利用这个快照数组构造一个新的构造器,因此这里遍历才不需要加锁,但是相对的,之后的add/remove/set等操作不会对迭代器造成任务影响,迭代器也不支持remove操作,也就不会抛出ConcurrentModificationException异常。

public E get(int index) {
        return get(getArray(), index);
    }

    public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }

总结

CopyOnArrayList使用与读多写少的场景,而且存储的对象最好不要太多,加入CopyOnArrayList中存储的数据比较多,那么每一次修改才做都会造成一次大对象拷贝,造成YGC甚至是FULL GC,因此使用前一定要考虑好场景。另外一个是由于读取都是快照读,因此会存在一定的延时造成读取不到最新的数据。

Flag Counter

走进Service mesh

最近几年微服务渐渐流行起来,我们开始慢慢的在将项目重构为micro services, 将每个project拆分为更细粒度的micro service, 利用Spring Clould、Netflix OSS 这些流行的框架,好像只需几行代码,甚至只要几个简单的注解就能够搞定日志、分布式追踪、监控、流控等,但是,there is alway a but :

  1. 试过的都知道,这些框架复杂性还是有点高的(https://netflix.github.io)
  2. 如果有其他团队想使用其他语言,那我们难道也要去用PHP、Go都重新造一遍轮子?
  3. 假设我们有几十上百个服务,每个服务又有几十上百个实例,我们如果需要升级某一个框架,加上每个服务在不停地迭代变化,这个时候需要在飞行中换引擎是相当复杂的流程。

显然我们需要更好的方案,这时候出现了Service Mesh,那么问题来了

什么是 Service Mesh

Service Mesh是一层专门用来处理服务于服务之间通讯的专用基础设施,负责在复杂的服务拓扑之间保证可靠的请求传递。在实践中通常实现为一组轻量级的代理,和服务本身部署在一起,而代理本身对应用程序是透明的(当然也有很多其他的变种)。

Service Mesh能做什么

Service Mesh有点类似网络模型,构建在TCP/IP之上,我们知道TCP能够保证基于字节的可靠传输,而不会去管具体传输的字节是HTTP还是protocol buffer其他协议,service mesh也是一样,他负责的是可靠的传输服务到服务之间的通讯,他也不会关心具体的载体是如何编码的,当然也类似TCP,service mesh也需要保证服务的可靠传递,例如重试、超时机制等,但是service mesh能做的远不止如此。

为了保证服务被可靠的传输,我们需要做相当多的工作,服务断流、重试、超时、监控、服务发现、负载均衡等等,当用Spring Clould的时候这一切都可以透明的用注解实现,例如:

@SpringBootApplication
@EnableDiscoveryClient//一个注解就实现了服务发现
@EnableCircuitBreaker
public class Application {

    public static void main(String[] args) {
        new SpringApplicationBuilder(Application.class).web(true).run(args);
    }

}

@Component
public class StoreIntegration {

    @HystrixCommand(fallbackMethod = "defaultStores")
    public Object getStores(Map<String, Object> parameters) {
        //可能失败的请求
    }

    public Object defaultStores(Map<String, Object> parameters) {
        return /*返回一些默认的内容*/;
    }
}

但是如果有些服务是用其他语言实现的呢,那么我们就需要重新造这些轮子,而如果采用service mesh的模式,我们就可以完全可以用GO实现通讯服务,java实现用户服务,我们只需要关心业务逻辑, 同时又可以无缝的整合这些所需的黑科技。

image

在这种模式之下,我们每个服务都会有一个agent部署在本机,服务于服务之间的通讯将都会通过agent进行,这样部署的方式就类似一个网络
image

William Morgan 的CEO Buoyant发现并将这种模式成为mesh network,而在2017年的年初给出了Service mesh的定义:

A service mesh is a dedicated infrastructure layer for handling service-to-service communication. It’s responsible for the reliable delivery of requests through the complex topology of services that comprise a modern, cloud native application. In practice, the service mesh is typically implemented as an array of lightweight network proxies that are deployed alongside application code, without the application needing to be aware.

image
服务于服务之间的流量通过代理之间流通,但是这里会有一个控制面板掌控着所有的代理,从而可以实现访问控制、流控等。service mesh将每个孤立的组件抽象出来,组合一种类网络拓扑模型。
image

Service Mesh的实践

service mesh已经开始快速的流行起来,istio是其中比较流行的一个,我们尝试搭建一个环境:

kubectl taint nodes --all dedicated-
  1. `git clone https://github.com/kubernetes/charts.git`
    
  2. 安装helm,参考:https://docs.helm.sh/using_helm/#installing-helm
    image

  3. cd incubator/istio/ -> helm install .
    image

  4. 部署Bookinfo,https://istio.io/docs/guides/bookinfo.html
    image

  5. 根据各个服务的地址(文档有具体命令),去访问每个具体的服务(zipkin分布式追踪、grafana监控等),剩下的就是自己亲身体验啦

  • 到这里已经差不多了,过程中会踩到很多的坑,这里就不一一记录了
    image
    image

image

总结

在分布式系统中,系统的扩展性和可用性是很重要的,当依赖的底层服务不可用时,上层的服务需要决定处理的策略,是重试还是降级等,通过service mesh,我们可以将这些策略内置到代理中,我们发现,service mesh并不是一种新的技术,而更多的是将这些功能的职责转换给了其他角色,随着业务的增长,服务与服务之间的关系也变得越来越复杂,而不再是一种简单的线性关系,更像是一张大网,类似Netflix的一些公司开发出了比较流行的框架尝试解决这些问题(Hystrix,Encukra等),类似Docker、Kubernetes的容器,不仅仅屏蔽的环境的差异,也提供了类似服务编排,但是随着服务之前复杂性的提升,容器编排也变得很复杂,于是有些人就开始把这些通用的功能都抽象到一层,作为一个透明的代理,对服务来说是无感的。

同时我们可以想想一下,因为应用的出口、入口流量都会首先经过sidecar,也就是说我们可以在这层解析它所转发的流量,比如说MySQL,我们解析出来得到一条SQL语句,那么我们就可以直接在这一层做分库分表中间件的工作,对于分库分表的策略我们可以通过统一的配置中心下发,当然这里会有问题,例如说额外的性能开销、资源占用等,但也正是如此多技术人投身于新技术的原因之一,有这么多问题亟待解决,这个过程会充满挑战。

Service mesh也是近一两年刚刚出现的技术,后面的发展方向还是有很多的不确定性,目前最大的好处能够
让你编码时不用再去关心服务降级、监控等这些微服务相关的组建技术,另一方面终于能够根据语言的优劣势来做技术选型,而不是绑定到某个单一的平台。

Flag Counter

消息队列实现概要——深度解读分区Topic的实现

前段时间也写过几篇关于消息队列的博客, 分布式消息队列实现概要 这篇博客大致讲了一下实现一个分布式消息队列所需要考虑到的种种因素,本文就详细讲一下如何实现Partitioned topic,即分区消息队列

前言

说道message queue相信大家都不陌生,对于业务方来说很简单就是几个简单的API, 通常也不用关心其内部实现,但如果想要用好消息队列、出了问题能够cover的住,那就必须要能够了解其实现原理,知其然知其所以然。
首先Message queue有一个topic的概念,可以将其理解为日志,发消息的过程说白了其实就是打日志,而消息队列就是存储日志记录的持久层,类似java中的log4j、logback等日志系统,打了日志之后我们就会有一些分析需求,比如说用户过来一个请求,我们将参数、耗时、响应内容等打到日志中,然后会在本机部署一个agent用于抓取日志发送到jstorm集群中用于分析等。那这里我们的应用系统就对应消息队列中的生产者producer,jstorm集群就对应消费者consumer。
所以说白了其实消息队列和log4j这些组件都是日志系统,那么回到message queue的模型中去,如果我们的topic都是单分区的,那么也就是说所有的producer都是往这一个"文件"中打日志,那么这样的话性能必然会有很大的问题,类似日志系统,我们希望每台机器都能够有自己的日志文件,那么自然而然的我们就需要将单分区的topic扩展到多分区的topic。

生产者

生产者这边的实现相对来说比较简单,对于应用层来说我们提供一个统一的topic,比如说hello-world, 那么创建topic的时候,我们对内表示的时候会加一层映射hello-world-> [hello-world-1,hello-world-2...],也就是说producer实际上是往对应的子topic发送消息的,但具体往哪个发送就需要一个路由策略,一般不要求顺序的话就直接轮训发送,如果需要顺序的话就用hash即可,这个信息直接存储到zookeeper即可,说到zookeeper这里简单提一下,zk并不能给客户单提供全局一致的视图,就是说对于两个不同的客户端,zk并不能保证他们能够在任一时间都能够读到完全相同的数据,这可能是由于网络延迟等原因造成,但如果客户端需要的话可以主动调用sync()先强制同步一把数据。
启动流程:

  1. 和broker建立连接
  2. 获取所有的topic列表
  3. 根据指定的路由策略发送消息

消费者

消费者这边的话会比较麻烦一些,根据消息队列要提供的消费语义有不同的实现方案,如果要实现顺序性的话相对会复杂一些。
流程:

  1. 消费者和broker建立连接
  2. 获取topic的元数据,例如topic分区数、offset等
  3. 根据不同的语义这里就需要连接到不同的分区
    实现的区别主要是由于第三点造成的,下面我们看一下主流的几款mq是如何实现的

RocketMQ

RocketMQ的consumer使用一般是均衡消费的方式,比如有一个topic: hello-world,我们创建的时候分配了16个分区,而我们总共有4个consumer:A/B/C/D,那么就会每个consumer分配四个分区,比如consumer A就会分配: hello-world-1、hello-world-2、hello-world-3、hello-world-4这四个分区。
也就是说总共需要这么几个要素: topic的总分区数、consumer的总数。topic的总分区数比较简单,启动的时候直接从zk读取即可,关键是第二个consumer的总数,我们知道应用是会宕机的、扩容、缩容、网络问题等,都会影响consumer的总数,RocketMQ是采用的方式是从name server中获取,每个节点启动的时候会定时向name server发送心跳,如果一定时间内没收到心跳包就可以判定这个节点挂掉了。那么这个问题解决了,consumer启动的时候可以从name server 发送请求,获取consumer的列表。
RocketMQ的分配分区是在客户端执行的,也就是说consumer获取到全量的consumer列表后,根据一定的策略分配分区,然后开始消费。然后我们知道consumer会扩容、缩容等原因不停的在变化,那么这个时候就需要重新负载均衡,那么客户端如果知道consumer变化了呢,有两种方式:

  1. broker主动通知
  2. 客户端定时刷新

那么这里就会有几个问题,由于网络延迟、每个consumer启动的时间不一样,那么consumer获取到变化的时机就会不一样,也就是说数据会不一致,导致在某段时间内 某个分区被多个客户端同时消费。

Kafka

kafka最初是基于zookeeper实现的,简单来说,每个客户端都会连接zookeeper,然后建立对应的watch事件,如果某一个consumer挂掉或者新增了consumer的话,zk下对应的路径就会有变化,然后产生watch事件并通知给客户端,客户端然后执行rebalance事件,看上去貌似方案还阔以,但是会有几个问题:

  1. 脑裂问题: consumer进行rebalance的时机都是根据zk的watch事件决定的,但是由于上文说所说的问题,zk并不能保证同时全局一致的视图,不同consumer看到的数据就不会不一致,导致执行rebalance的时机不一致。
  2. 由于网络延迟,客户端不可能同时接受到watch事件,因此执行rebalance的时机会有一个小窗口
  3. 羊群效应: 一个consumer节点有变化,所有监听这个路径的consumer都会得到通知,但客户端时机可能并不关心这个事件,这样就会导致占用大量的带宽,导致其他操作延迟。

由于上述原因kafka后面进行了几次改版,核心**就是将收集consumer全量列表的过程放到了server端,然后将rebalance的过程放到了客户端,之前有一版是两个过程都在server端,但是这样就会有个问题:由于rebalance的过程放到了服务器端,那么如果想要指定自定义的分配策略就不灵活了,比如说想要根据机架、机房等分配,因此后面将这个过程放到了客户端。
流程:

  1. Joining the Group
  2. Synchronizing Group State

consumer启动后,会首先向broker发送请求查询当前consumer group的GroupCoordinator,然后就会进入
Joining the Group阶段,向GroupCoordinator发送JoinGroupRequest,GroupCoordinator会从中选一个成为leader,然后向所有的节点发送响应,表明加入成功,但是只有leader的响应中才会包含所有的consumer元数据。
然后会进入Synchronizing Group State阶段, 每个consumer会向GroupCoordinator发送SyncGroup请求,其中leader的SyncGroup请求包含了分配的结果,等leader收到了分配的结果后就会发送响应将结果同步给所有的consumer。当然这里会有很多的corner case,比如说如果GroupCoordinator挂掉了咋办?consumer leader挂掉了又咋办?这里就不详细叙述了。

Pulsar

pulsar的话相对比较清晰,pulsar支持三种消费语义,详细可以参考之前的博客,其中一个shared模式,或者叫RoundRobin模式,所有的consumer可以连接到同一个topic,然后消息会以RoundRobin的形式,轮训发送给每一个consumer,一个消息只会下发给一个consumer。如果一个consumer挂掉了,所有发给它但是还没有ack的消息,都会重新调度发给其他consumer。
看起来实现还是比较清晰的,但是不支持顺序性,因此需要看一下自己的业务场景是否可以满足。

image

总结

这篇博客总结了一下如何实现分区消息队列,并分析了一下主流的几款mq的实现方式,没不存在一种最完美的方案,按照自己的业务场景,比如说是否要求顺序性、性能、是否需要自定义rebalance策略等,决定自己的消息队列的实现方式。

Flag Counter

Struts2-初始化流程

Apache Struts is a free, open-source, MVC framework for creating elegant, modern Java web applications. It favors convention over configuration, is extensible using a plugin architecture, and ships with plugins to support REST, AJAX and JSON.

运行主线

入口程序

StrutsPrepareAndExecuteFilter是Struts2的入口点,实现了Filter和StrutsStatics接口,其中StrutsStatics定义了一些常量

public interface StrutsStatics {

    /**
     * Constant for the HTTP request object.
     */
    public static final String HTTP_REQUEST = "com.opensymphony.xwork2.dispatcher.HttpServletRequest";

   	...
   	...
    /**
     * Set as an attribute in the request to let other parts of the framework know that the invocation is happening inside an
     * action tag
     */
    public static final String STRUTS_ACTION_TAG_INVOCATION= "struts.actiontag.invocation";
}

而实现了Filter接口,让Struts2能够过滤请求,如静态资源、Servlet等,在doFilter()方法中实现过滤逻辑,而init()方法会在且只在Filter被初始化的时候被调用一次,让我们来看看StrutsPrepareAndExecuteFilter的init()方法

protected PrepareOperations prepare; 
protected ExecuteOperations execute;	
protected List<Pattern> excludedPatterns = null;

public void init(FilterConfig filterConfig) throws ServletException {
        InitOperations init = new InitOperations();//类似一个工具类,包含了一些初始化操作
        Dispatcher dispatcher = null;//Dispatcher:Struts2的核心分发器
        try {
            /**
             * 封装filterConfig,提供了一个便利的方法
             * getInitParameterNames(),将枚举类型的参数转换成Iterator(EnumerationIterator)
             */
            FilterHostConfig config = new FilterHostConfig(filterConfig);
            init.initLogging(config);//初始化日志
            //初始化Dispatcher
            dispatcher = init.initDispatcher(config);
            init.initStaticContentLoader(config, dispatcher);//初始化静态文件加载器

            prepare = new PrepareOperations(dispatcher);//初始化HTTP预处理的操作类
            execute = new ExecuteOperations(dispatcher);//初始化进行HTTP请求处理的逻辑执行操作类
            this.excludedPatterns = init.buildExcludedPatternsList(dispatcher);

            postInit(dispatcher, filterConfig);//回调方法,留作用户拓展
        } finally {
            if (dispatcher != null) {
                dispatcher.cleanUpAfterInit();
            }
            init.cleanup();
        }
    }

初始化核心分发器:Dispatcher

init()方法主要是对Dispatcher,PrepareOperations,ExecuteOperations三个类进行初始化,其中Dispatcher在Struts2中占有很重要的地位,无论是初始化Struts2还是对HTTP请求的处理,同时也架起了Struts2和XWork之间的一道桥梁,因此我们先深入 dispatcher = init.initDispatcher(config); 这段代码看一看

    public Dispatcher initDispatcher( HostConfig filterConfig ) {
        Dispatcher dispatcher = createDispatcher(filterConfig);
        dispatcher.init();//初始化方法
        return dispatcher;
    }

    private Dispatcher createDispatcher( HostConfig filterConfig ) {
        //将filterConfig中的参数名值对封装到Map中
        Map<String, String> params = new HashMap<String, String>();
        for ( Iterator e = filterConfig.getInitParameterNames(); e.hasNext(); ) {
            String name = (String) e.next();
            String value = filterConfig.getInitParameter(name);
            params.put(name, value);
        }
        return new Dispatcher(filterConfig.getServletContext(), params);
    }
    

上面的没什么,dispatcher.init();才是重头戏,继续深入

public void init() {
        //初始化配置文件管理器
    	if (configurationManager == null) {
            //根据name进行对象寻址
            //DEFAULT_BEAN_NAME = "struts"
            //<bean type="org.apache.struts2.dispatcher.DispatcherErrorHandler" name="struts".../>
            //<bean class="com.opensymphony.xwork2.ObjectFactory" name="struts"/>
            configurationManager = createConfigurationManager(DefaultBeanSelectionProvider.DEFAULT_BEAN_NAME);
    	}

        try {
            init_FileManager(); //初始化文件管理器

            // 初始化Struct2的默认配置加载器:
            // org/apache/struts2/default.properties,
            // 如果项目中需要覆盖,可以在classpath里的struts.properties里覆写
            init_DefaultProperties(); // [1]
            //初始化Xml配置加载器:
            // 如struts-default.xml,struts-plugin.xml,struts.xml
            init_TraditionalXmlConfigurations(); // [2]
            //初始化Properties配置加载器
            init_LegacyStrutsProperties(); // [3]
            //初始化用户自定义的配置加载器
            init_CustomConfigurationProviders(); // [5]
            //初始化由web.xml传入的参数
            init_FilterInitParameters() ; // [6]
            //初始化容器内置的对象
            //eg:ObjectFactory,FreemarkerManager....
            init_AliasStandardObjects() ; // [7]
            //创建容器, 初始化并预加载配置
            Container container = init_PreloadConfiguration();
            //对容器进行依赖注入
            container.inject(this);
            //检查对WebLogic的特殊支持
            init_CheckWebLogicWorkaround(container);
            //初始化所有的DispatcherListener
            if (!dispatcherListeners.isEmpty()) {
                for (DispatcherListener l : dispatcherListeners) {
                    l.dispatcherInitialized(this);
                }
            }
            //初始化错误处理器
            errorHandler.init(servletContext);

        } catch (Exception ex) {
            if (LOG.isErrorEnabled())
                LOG.error("Dispatcher initialization failed", ex);
            throw new StrutsException(ex);
        }
    }

    private void init_DefaultProperties() {
        configurationManager.addContainerProvider(new DefaultPropertiesProvider());
    }
    
    private void init_LegacyStrutsProperties() {
        configurationManager.addContainerProvider(new PropertiesConfigurationProvider());
    }

    private void init_TraditionalXmlConfigurations() {
        String configPaths = initParams.get("config");
        if (configPaths == null) {
            configPaths = DEFAULT_CONFIGURATION_PATHS;
        }
        String[] files = configPaths.split("\\s*[,]\\s*");
        for (String file : files) {
            if (file.endsWith(".xml")) {
                if ("xwork.xml".equals(file)) {
                    configurationManager.addContainerProvider(createXmlConfigurationProvider(file, false));
                } else {
                    configurationManager.addContainerProvider(createStrutsXmlConfigurationProvider(file, false, servletContext));
                }
            } else {
                throw new IllegalArgumentException("Invalid configuration file name");
            }
        }
    }
    //...其他的省略

ConfigurationProvider

所有的初始化方法都是以init_开头,其核心只不过是调用configurationManager#addContainerProvider()方法,那到底什么是配置元素加载器(ContainerProvider)呢,比如init_DefaultProperties(),看一下DefaultPropertiesProvider的继承关系

public class DefaultPropertiesProvider extends PropertiesConfigurationProvider
	->public class PropertiesConfigurationProvider implements ConfigurationProvider
		 ->public interface ConfigurationProvider extends ContainerProvider, PackageProvider

其他的ContainerProvider也都实现了ConfigurationProvider这个接口,我们知道Struts2的配置文件形式有很多种,比如.xml,.properties等,所以Struts2就定义了ConfigurationProvider这个统一的接口,让框架支持处理所有的配置形式,而每一个ContainerProvider的实现类都可以根据不同的配置文件的特点进行设计。
同时ConfigurationProvider继承了ContainerProvider和PackageProvider两个接口,ContainerProvider的子类有:FileManagerFactoryProvider,StubConfigurationProvider, XmlConfigurationProvider, BeanSelectionProvider等,它的用途大概就是处理诸如XML,Properties等格式的配置文件。而PackageProvider的操作对象是PackageConfig,从源码可以看出,PackageConfig对应了XML配置文件中的package节点,这样PackageProvider的作用也不言而喻

public class PackageConfig extends Located implements Comparable, Serializable, InterceptorLocator {

    private static final Logger LOG = LoggerFactory.getLogger(PackageConfig.class);

    protected Map<String, ActionConfig> actionConfigs;
    protected Map<String, ResultConfig> globalResultConfigs;
    protected Map<String, Object> interceptorConfigs;
    protected Map<String, ResultTypeConfig> resultTypeConfigs;
    protected List<ExceptionMappingConfig> globalExceptionMappingConfigs;
    protected List<PackageConfig> parents;
    protected String defaultInterceptorRef;
    protected String defaultActionRef;
    protected String defaultResultType;
    protected String defaultClassRef;
    protected String name;
    protected String namespace = "";
    protected boolean isAbstract = false;
    protected boolean needsRefresh;
    ...
}

初始化容器

Struts2中的所有内置对象都会交给Container去管理,比如XML中的Bean,Constant节点以及Properties文件中的参数,Container的实现类会扫描@Inject注解,进行依赖注入,下面我们看看Container container = init_PreloadConfiguration();这行代码做了什么事情

    private Container init_PreloadConfiguration() {
        Container container = getContainer();

        boolean reloadi18n = Boolean.valueOf(container.getInstance(String.class, StrutsConstants.STRUTS_I18N_RELOAD));
        LocalizedTextUtil.setReloadBundles(reloadi18n);

        boolean devMode = Boolean.valueOf(container.getInstance(String.class, StrutsConstants.STRUTS_DEVMODE));
        LocalizedTextUtil.setDevMode(devMode);

        return container;
    }
public Container getContainer() {
        if (ContainerHolder.get() != null) {
            return ContainerHolder.get();
        }
        //ConfigurationManager类对所有的配置管理中心
        ConfigurationManager mgr = getConfigurationManager();
        if (mgr == null) {
            throw new IllegalStateException("The configuration manager shouldn't be null");
        } else {
            Configuration config = mgr.getConfiguration();
            if (config == null) {
                throw new IllegalStateException("Unable to load configuration");
            } else {
                Container container = config.getContainer();
                ContainerHolder.store(container);
                return container;
            }
        }
    }

我们再看看Container的实现类ContainerImpl,它的内部缓存了两个实例factories和factoryNamesByType,而factories根据Key缓存了不同对象的制造工厂,Key中有两个变量:type,name,factoryNamesByType则在factories基础之上根据名称进行寻址。
getInstance()方法的每次调用,都会根据传进来的type,class构造一个Key对象,然后到factories中查找到对应的工厂类,调用Factory的create()方法,创建对象

class ContainerImpl implements Container {

	final Map<Key<?>, InternalFactory<?>> factories;
	final Map<Class<?>, Set<String>> factoryNamesByType;

	@SuppressWarnings("unchecked")
	<T> T getInstance( Class<T> type, String name, InternalContext context ) {
		ExternalContext<?> previous = context.getExternalContext();
		Key<T> key = Key.newInstance(type, name);
		context.setExternalContext(ExternalContext.newInstance(null, key, this));
		try {
			InternalFactory o = getFactory(key);
			if (o != null) {
				return getFactory(key).create(context);
			} else {
				return null;
			}
		} finally {
			context.setExternalContext(previous);
		}
	}

	<T> T getInstance( Class<T> type, InternalContext context ) {
		return getInstance(type, DEFAULT_NAME, context);
	}
	/..
}
class Key<T> {

  final Class<T> type;
  final String name;
  final int hashCode;
  ..
}
interface InternalFactory<T> extends Serializable {

  /**
   * Creates an object to be injected.
   *
   * @param context of this injection
   * @return instance to be injected
   */
  T create(InternalContext context);
}
    <bean type="com.opensymphony.xwork2.factory.ActionFactory" name="struts" class="com.opensymphony.xwork2.factory.DefaultActionFactory" />
    <bean type="com.opensymphony.xwork2.factory.ConverterFactory" name="struts" class="com.opensymphony.xwork2.factory.DefaultConverterFactory" />
    <bean type="com.opensymphony.xwork2.factory.InterceptorFactory" name="struts" class="com.opensymphony.xwork2.factory.DefaultInterceptorFactory" />
    <bean type="com.opensymphony.xwork2.factory.ValidatorFactory" name="struts" class="com.opensymphony.xwork2.factory.DefaultValidatorFactory" />
    <bean type="com.opensymphony.xwork2.factory.UnknownHandlerFactory" name="struts" class="com.opensymphony.xwork2.factory.DefaultUnknownHandlerFactory" />

PrepareOperations和ExecuteOperations分析

从源码中可以看出,PrepareOperations负责创建ActionContext,清理Request,设置编码等

pre_method

ExecuteOperations则只有两个方法,负责真正的执行操作,executeStaticResourceRequest()负责静态资源,executeAction()是一个代理方法,将真正的执行交给Dispatcher.serviceAction()方法

 public boolean executeStaticResourceRequest(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        // there is no action in this request, should we look for a static resource?
        String resourcePath = RequestUtils.getServletPath(request);

        if ("".equals(resourcePath) && null != request.getPathInfo()) {
            resourcePath = request.getPathInfo();
        }

        StaticContentLoader staticResourceLoader = dispatcher.getContainer().getInstance(StaticContentLoader.class);
        if (staticResourceLoader.canHandle(resourcePath)) {
            staticResourceLoader.findStaticResource(resourcePath, request, response);
            // The framework did its job here
            return true;

        } else {
            // this is a normal request, let it pass through
            return false;
        }
    }

    /**
     * Executes an action
     * @throws ServletException
     */
    public void executeAction(HttpServletRequest request, HttpServletResponse response, ActionMapping mapping) throws ServletException {
        dispatcher.serviceAction(request, response, mapping);
    }

总结

  1. 在Servlet容器(Jetty,Tomcat...)初始化的时候,加载web.xml初始化Filter
  2. 初始化StrutsPrepareAndExecuteFilter,调用init()方法
    (1) 封装FilterConfig->FilterHostConfig
    (2) 初始化日志操作
    (3) 初始化Dispatcher
    (4) 初始化PrepareOperations和ExecuteOperations

Flag Counter

Java原生类型包装类初解析

首先看一段代码

        Integer a = 126;
        Integer b  =126;
        Integer c = 129 ;
        Integer d = 129 ;
        System.out.println(a==b);
        System.out.println(c==d);

输出结果会是多少呢,相信每个人心中都会有自己的答案,倒不如直接运行一下看看:

true
false

现在就有了疑问,为什么两个输出的结果不一样呢,这里就设计到了Integer设计了,我们可以用javap命令反编译一下字节码,看看到底发生了什么:

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=5, args_size=1
         0: bipush        126
         2: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         5: astore_1
         6: bipush        126
         8: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        11: astore_2
        12: sipush        129
        15: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        18: astore_3
        19: sipush        129
        22: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        25: astore        4
        27: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        30: aload_1
        31: aload_2
        32: if_acmpne     39
        35: iconst_1
        36: goto          40
        39: iconst_0
        40: invokevirtual #4                  // Method java/io/PrintStream.println:(Z)V
        43: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        46: aload_3
        47: aload         4
        49: if_acmpne     56
        52: iconst_1
        53: goto          57
        56: iconst_0
        57: invokevirtual #4                  // Method java/io/PrintStream.println:(Z)V
        60: return

从字节码中可以看出是调用了Integer类的静态方法valueOf(),因此我们再进去Integer的源码研究一下:

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

那么这里的IntegerCache类有什么秘密呢,我们也进去看一下源码,IntegerCache是Integer的一个私有内部类,构造器也是
私有的,保证了安全性,通过源码可以看出,low的值默认为-128,修饰符为static final,因此不可再更改其值,而high
的值可以通过设置参数-XX:AutoBoxCacheMax来设置,同时内部维护了一个static finalInteger数组.

private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];

        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);

            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }

可以看出如果大于IntegerCache.low并且小于IntegerCache.high的话,则直接返回缓存中的对象,
因此用==比较肯定是相等的,如果if的条件不成立的话,那么就会new一个新的Integer对象返回,
因此肯定是不相等的
对于Boolean则是内部维护了两个常量:

    public static final Boolean TRUE = new Boolean(true);
    public static final Boolean FALSE = new Boolean(false);

对于Short和Long其实和Integer的实现差不多,只不过缓存的范围写死在了代码里面:

 final int offset = 128;
        if (l >= -128 && l <= 127) { // will cache
            return LongCache.cache[(int)l + offset];
        }
        return new Long(l);

对于Double和Float来说,则是直接返回,不进行任何缓存的操作:

    public static Double valueOf(double d) {
        return new Double(d);
    }

Flag Counter

API网关异步化改造技术选型

背景

目前的网关是基于Spring Boot 1.5.xTomcat 8.5.x构建,采用多线程阻塞模型,也就是说每个请求都会占用一个独立的线程资源,而线程在JVM中是一个相对比较重的资源。当应用是CPU密集型的或者说依赖的远程服务都正常工作时,这种模型能够很好的满足需求,但一旦后端服务出现了延迟,比如慢查询、FullGC、依赖的第三方接口出问题等情况,线程池很容易被打满,使得整个集群服务出现问题。典型的IO密集型的应用也会有类似的问题,比如网关有很多HTTP请求、RPC远程调用等,当并发量比较大的时候,线程都阻塞在IO等待上,造成线程资源的浪费。

这种模型的优势比较明显:

  • 编程模型简单
  • 易于开发、调试、运维等。本地调试问题支持直接打断点、通过ThreadLocal变量实现监控、通过thread dump即可获取当前请求的处理流程等

但劣势也很明显:

  • 连接数限制。容器的最大线程数一般是固定的,tomcat默认是200,因此当发生网络延迟、FullGC、第三方服务慢等情况造成上游服务延迟时,线程池很容易会被打满,造成新的请求被拒绝,但这个时候其实线程都阻塞在IO上,系统的资源被没有得到充分的利用。

tomcat默认可以接收10000个连接,worker线程默认为200,当线程池被打满后,poller线程会继续接收新的连接请求,并放到epoll队列中,当超过最大连接数后,则会拒绝响应,虽然Tomcat采用了NIO模型,但由于业务线程是同步处理的的,因此当并发比较高时,很容易造成线程池被打满。
image

  • 容易受网络、磁盘IO等延迟影响。需要谨慎设置超时时间,如果设置不当,且接口之前的隔离做的不是很完善,则服务很容易被一个延迟的接口拖垮。

而异步化的方式则完全不同,通常情况下一个CPU核启动一个线程即可处理所有的请求、响应。一个请求的生命周期不再固定于一个线程,而是会分成不同的阶段交由不同的线程池处理,系统的资源能够得到更充分的利用。而且因为线程不再被某一个连接独占,一个连接所占用的系统资源也会低得多,只是一个文件描述符加上几个监听器,而在阻塞模型中,每条连接都会独占一个线程,是一个非常重的资源。对于上游服务的延迟情况,能够得到很大的缓解,因为在阻塞模型中,慢请求会独占一个线程资源,而异步化之后,因为单条连接诶所占用的资源变的非常低,因此系统可以同时处理大量的请求。

因此考虑对网关进行异步化改造,解决当前遇到的超时、延迟等问题。

技术选型

Zuul 2

Zuul 2基于Netty和RxJava实现,采用了异步非阻塞模型,本质上其实就是队列+事件驱动。在zuul 1中一个请求的完整生命周期都是在一个线程中完成的,但在zuul 2中,请求首先会经过netty server,接着会运行前置拦截器,然后通过netty客户端将请求转发给后端的服务,最后运行后置拦截器并返回响应。但是和zuul 1不同,这里的拦截器同时支持异步和同步两种模式,对于一些比较快的操作,可以直接使用同步拦截器。

image

异步拦截器示例:

class SampleServiceFilter extends HttpInboundFilter {
    private static final Logger log = LoggerFactory.getLogger(SampleServiceFilter.class)

    private final SampleService sampleService

    @Inject
    SampleServiceFilter(SampleService sampleService) {
        this.sampleService = sampleService
    }

    @Override
    int filterOrder() {
        return 500
    }


    @Override
    boolean shouldFilter(HttpRequestMessage msg) {
        return sampleService.isHealthy()
    }

    @Override
    Observable<HttpRequestMessage> applyAsync(HttpRequestMessage request) {
        //模拟慢请求
        return sampleService.makeSlowRequest().map({ response ->
            log.info("Fetched sample service result: {}", response)

            return request
        })
    }
}

这里返回的是一个Observable,这是RxJava中的概念,和Java8的CompletableFuture有点像,对于方法调用者来说拿到的都是一个Observable,而内部的实现方式可以是同步,也可以是异步,但是调用者不用关心这个东西,无论实现怎么改,方法的签名是不用变的,始终返回的都是一个Observable

关于响应式的概念这里就不多做介绍了,我觉得上手还是有点难度,个人更倾向于coroutine的方案。

String[] names = ...;
Observable.from(names)
    .subscribe(new Action1<String>() {
        @Override
        public void call(String name) {
            Log.d(tag, name);
        }
    });

Zuul 2是一个不错的选择,但是spring官方已经不打算集成zuul 2了,加上Netflix也打算把技术栈尽可能的迁移到Spring,hystrix和Eureka也都进入维护状态,不再开发新特性,zuul未来也有可能是同样的命运。

Moving forward, we plan to leverage the strong abstractions within Spring to further modularize and evolve the Netflix infrastructure. Where there is existing strong community direction — such as the upcoming Spring Cloud Load Balancer — we intend to leverage these to replace aging Netflix software. Where there is new innovation to bring — such as the new Netflix Adaptive Concurrency Limiters — we want to help contribute these back to the community.

基于Servlet3.1的异步

image

Servlet3.1引入了非阻塞式编程模型,支持请求的异步处理。

public void doGet(request, response) {
        ServletOutputStream out = response.getOutputStream();
        AsyncContext ctx = request.startAsync();
        //异步写入
        out.setWriteListener(new WriteListener() {
            void onWritePossible() {
                while (out.isReady()) {
                    byte[] buffer = readFromSomeSource();
                    if (buffer != null)
                        out.write(buffer); ---> Async Write!
                    else{
                        ctx.complete(); break;
                    }
                  }
                }
            });
        }

Spring 4.x+也增加了对非阻塞式IO的支持,例如下面的代码示例(SpringMVC5 + Tomcat 8.5+):

    @GetMapping(value = "/asyncNonBlockingRequestProcessing")
    public CompletableFuture<String> asyncNonBlockingRequestProcessing(){
            ListenableFuture<String> listenableFuture = getRequest.execute(new AsyncCompletionHandler<String>() {
                @Override
                public String onCompleted(Response response) throws Exception {
                    logger.debug("Async Non Blocking Request processing completed");
                    return "Async Non blocking...";
                 }
            });
            return listenableFuture.toCompletableFuture();
    }
    
@PostMapping
public Callable<String> processUpload(final MultipartFile file) {

    return new Callable<String>() {
        public String call() throws Exception {
            // ...
            return "someView";
        }
    };

}    

虽然说Servlet3.1提供了对异步的支持,但是其编程模型本质上还是同步的:Filter, Servlet, 或者有一些方法仍然是阻塞的,比如getParameter, getPart等,解析请求体、写会响应本质上还是同步的,但一般来说性能损耗也不算大,网关的耗时基本上都在业务方的IO调用上。

Spring 5 Reactive

对于异步编程模型的选择,Spring5中引入了两种方式,一种是构建于Servlet 3.1之上的SpringMVC,另一种是构建于Netty之上的Spring WebFluxSpring WebFlux不同于Spring MVC,是一个专门为异步设计的响应式框架,完全非阻塞,支持响应式编程模型,可以运行在 Netty, Undertow, 和 Servlet 3.1+容器中。

image

不同于SpringMVC,WebFlux的请求体、响应都支持响应式类型,可以异步的接受、写入响应,是一个完全异步化的框架。

@PostMapping("/accounts")
public void handle(@RequestBody Mono<Account> account) {
    // ...
}

@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public Mono<String> processSubmit(@Valid @ModelAttribute("pet") Mono<Pet> petMono) {
    return petMono
        .flatMap(pet -> {
            // ...
        })
        .onErrorResume(ex -> {
            // ...
        });
}

另外Spring WebFlux也提供了一个响应式、非阻塞的HTTP客户端:WebClient. 其内部支持多种实现,默认是Reactor Netty,也支持Jetty reactive HttpClient,当然也可以自己通过ClientHttpConnector扩展。

Mono<Void> result = client.post()
            .uri("/persons/{id}", id)
            .contentType(MediaType.APPLICATION_JSON)
            .body(personMono, Person.class)
            .retrieve()
            .bodyToMono(Void.class);

Spring Cloud Gateway

Spring Cloud Gateway是由spring官方基于Spring5.0、Spring Boot2.0、Project Reactor等技术开发的网关,目的是代替原先版本中的Spring Cloud Netfilx Zuul,目前Netfilx已经开源了Zuul2.0,但Spring没有考虑集成,而是推出了自己开发的Spring Cloud GateWay。该项目提供了一个构建在Spring生态系统之上的API网关。
特性:

  • 基于Spring Framework 5, Project Reactor 和 Spring Boot 2.0
  • 能够根据请求的任何属性匹配路由
  • 支持Hystrix
  • 支持Spring Cloud DiscoveryClient
  • 限流
  • 路径重写
  • 过滤器
@SpringBootApplication
public class DemogatewayApplication {
	@Bean
	public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
		return builder.routes()
			.route("path_route", r -> r.path("/get")
				.uri("http://httpbin.org"))
			.route("host_route", r -> r.host("*.myhost.org")
				.uri("http://httpbin.org"))
			.route("rewrite_route", r -> r.host("*.rewrite.org")
				.filters(f -> f.rewritePath("/foo/(?<segment>.*)", "/${segment}"))
				.uri("http://httpbin.org"))
			.route("hystrix_route", r -> r.host("*.hystrix.org")
				.filters(f -> f.hystrix(c -> c.setName("slowcmd")))
				.uri("http://httpbin.org"))
			.route("hystrix_fallback_route", r -> r.host("*.hystrixfallback.org")
				.filters(f -> f.hystrix(c -> c.setName("slowcmd").setFallbackUri("forward:/hystrixfallback")))
				.uri("http://httpbin.org"))
			.route("limit_route", r -> r
				.host("*.limited.org").and().path("/anything/**")
				.filters(f -> f.requestRateLimiter(c -> c.setRateLimiter(redisRateLimiter())))
				.uri("http://httpbin.org"))
			.build();
	}
}

自研

另外也可以参考Zuul2、Spring Cloud Gateway等,基于Netty、Vertx或者spring4.x提供的基于Servlet 3.1的异步机制自研,但自研成本会很高,需要从零开始开发。

对比

选型 优势 劣势
Zuul 2 特性完善。重试、并发保护等 Spring官方不打算集成,需要自己搞。后期项目的活跃度,Netflix开源的eureka、hystrix都进入了维护模式
Spring Boot 1.x + Spring 4.x Servlet 3.1 部分支持异步 如果目前是基于传统spring mvc的方式,相对改造成本比较小
Spring Boot 2 + Spring MVC 部分支持异步 需要升级Spring Boot 2
Spring Boot 2 + Spring Web Flux 完全异步化、异步Http客户端的WebClient 需要升级Spring Boot 2
自研 能够更好的和业务结合 成本太高

问题

需要特别注意的一些问题:

  • 异步化之后,整个流程都是基于事件驱动,请求处理的流程随时可能被切换断开,需要通过trace_id等机制才能把整个执行流再串联起来,给开发、调试、运维等引入了很多复杂性,比如想在IDE里面通过打断点排查问题就不是很方便了。
  • 整个流程都是基于事件驱动,代码相对而言会变得更复杂,想梳理清楚整个工作流程会更麻烦,同步的方式只要跟着IDE一步一步点进去就可以。
  • ThreadLocal机制在异步化之后就不能很好的工作了。Netflix也遇到了很多ThreadLocal的问题,比如监控、traceId的传递、业务参数的传递等,这个需要特别注意。
  • 异步的编程模式,采用回调、future还是响应式? 更激进一点可以考虑下kotlin的coroutine

总结

网关的异步化改造相对还是比较必要的,作为所有流量的入口,性能、稳定性是非常重要的一环,另外由于网关接入了内部所有的API,因此在大促时需要进行比较完善的压测,评估网关的容量,并进行扩容,但如果内部的业务比较复杂,网关接入了非常多的API,这种中心化的方案就会导致很难对网关进行比较准确的容量评估,后面可以考虑基于Service Mesh的**,对网关进行去中心化改造,将网关的核心逻辑,比如鉴权、限流、协议转换、计费、监控、告警等都抽到sidecar中。

参考链接

深度解析Java线程池的异常处理机制

前言

今天小伙伴遇到个小问题,线程池提交的任务如果没有catch异常,那么会抛到哪里去,之前倒是没研究过,本着实事求是的原则,看了一下代码。

正文

小问题

考虑下面这段代码,有什么区别呢?你可以猜猜会不会有异常打出呢?如果打出来的话是在哪里?:

        ExecutorService threadPool = Executors.newFixedThreadPool(1);
        threadPool.submit(() -> {
            Object obj = null;
            System.out.println(obj.toString());
        });
        threadPool.execute(() -> {
            Object obj = null;
            System.out.println(obj.toString());
        });

源码解析

我们下面就来看下代码, 其实就是将我们提交过去的Runnable包装成一个Future

	public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }
    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
    }
    public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // volatile修饰,保证多线程下的可见性,可以看看Java内存模型
    }
    public static <T> Callable<T> callable(Runnable task, T result) {
        if (task == null)
            throw new NullPointerException();
        return new RunnableAdapter<T>(task, result);
    }
	
    static final class RunnableAdapter<T> implements Callable<T> {
        final Runnable task;
        final T result;
        RunnableAdapter(Runnable task, T result) {
            this.task = task;
            this.result = result;
        }
        public T call() {
            task.run();
            return result;
        }
    }

接下来就会实际提交到队列中交给线程池调度处理:

	/**
	* 代码还是很清爽的,一个很典型的生产者/消费者模型,
	* 这里暂不纠结这些细节,那么如果提交到workQueue成功的话,消费者是谁呢?
	* 明显在这个newWorker里搞的鬼,同样细节有兴趣可以自己再去研究,这里我们会发现
	* 核心就是Worker这个内部类
	*/
	public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

那么接下来看看线程池核心的流程:

private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable{
          /** Delegates main run loop to outer runWorker  */
        public void run() {
            runWorker(this);
        }
}

final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
          	//getTask()方法会尝试从队列中抓取数据
            while (task != null || (task = getTask()) != null) {
                w.lock();
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                  	//可覆写此方法打日志埋点之类的
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        //简单明了,直接调用run方法
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

submit的方式

那么我们可以这里是直接调用的run方法,先看submit的方式,我们知道最终传递过去的是一个FutureTask,也就是说会调用这里的run方法,我们看看实现:

	public void run() {
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                  	//。。。
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        } finally {
          //省略
    }
  
      protected void setException(Throwable t) {
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = t; //赋给了这个变量
            UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
            finishCompletion();
        }
    }

可以看到其实类似于直接吞掉了,这样的话我们调用get()方法的时候会拿到, 比如我们可以重写afterExecute方法,从而可以得到实际的异常:

protected void afterExecute(Runnable r, Throwable t) {
          super.afterExecute(r, t);
          if (t == null && r instanceof Future<?>) {
            try {
              //get这里会首先检查任务的状态,然后将上面的异常包装成ExecutionException
              Object result = ((Future<?>) r).get();
            } catch (CancellationException ce) {
                t = ce;
            } catch (ExecutionException ee) {
                t = ee.getCause();
            } catch (InterruptedException ie) {
                Thread.currentThread().interrupt(); // ignore/reset
            }
          }
          if (t != null){
            //异常处理
            t.printStackTrace();
          }
        }

execute的方式

那么如果是直接exeture的方式有啥不同呢?这样的话传递过去的就直接是Runnable,因此就会直接抛出:

    try {
        task.run();
    } catch (RuntimeException x) {
        thrown = x; throw x;
    } catch (Error x) {
        thrown = x; throw x;
    } catch (Throwable x) {
        thrown = x; throw new Error(x);
    } finally {
        afterExecute(task, thrown);
    }

那么这里的异常到底会抛出到哪里呢, 我们看看JVM具体是怎么处理的:

if (!destroy_vm || JDK_Version::is_jdk12x_version()) {
    // JSR-166: change call from from ThreadGroup.uncaughtException to
    // java.lang.Thread.dispatchUncaughtException
    if (uncaught_exception.not_null()) {
      //如果有未捕获的异常
      Handle group(this, java_lang_Thread::threadGroup(threadObj()));
      {
        KlassHandle recvrKlass(THREAD, threadObj->klass());
        CallInfo callinfo;
        KlassHandle thread_klass(THREAD, SystemDictionary::Thread_klass());
        /*	
        	这里类似一个方法表,实际就会去调用Thread#dispatchUncaughtException方法
        	template(dispatchUncaughtException_name,            "dispatchUncaughtException")                
        */
        LinkResolver::resolve_virtual_call(callinfo, threadObj, recvrKlass, thread_klass,
                                           vmSymbols::dispatchUncaughtException_name(),
                                           vmSymbols::throwable_void_signature(),
                                           KlassHandle(), false, false, THREAD);
        CLEAR_PENDING_EXCEPTION;
        methodHandle method = callinfo.selected_method();
        if (method.not_null()) {
          JavaValue result(T_VOID);
          JavaCalls::call_virtual(&result,
                                  threadObj, thread_klass,
                                  vmSymbols::dispatchUncaughtException_name(),
                                  vmSymbols::throwable_void_signature(),
                                  uncaught_exception,
                                  THREAD);
        } else {
          KlassHandle thread_group(THREAD, SystemDictionary::ThreadGroup_klass());
          JavaValue result(T_VOID);
          JavaCalls::call_virtual(&result,
                                  group, thread_group,
                                  vmSymbols::uncaughtException_name(),
                                  vmSymbols::thread_throwable_void_signature(),
                                  threadObj,           // Arg 1
                                  uncaught_exception,  // Arg 2
                                  THREAD);
        }
        if (HAS_PENDING_EXCEPTION) {
          ResourceMark rm(this);
          jio_fprintf(defaultStream::error_stream(),
                "\nException: %s thrown from the UncaughtExceptionHandler"
                " in thread \"%s\"\n",
                pending_exception()->klass()->external_name(),
                get_thread_name());
          CLEAR_PENDING_EXCEPTION;
        }
      }
    }

可以看到这里最终会去调用Thread#dispatchUncaughtException方法:

    private void dispatchUncaughtException(Throwable e) {
      	//默认会调用ThreadGroup的实现
        getUncaughtExceptionHandler().uncaughtException(this, e);
    }

    public void uncaughtException(Thread t, Throwable e) {
        if (parent != null) {
            parent.uncaughtException(t, e);
        } else {
            Thread.UncaughtExceptionHandler ueh =
                Thread.getDefaultUncaughtExceptionHandler();
            if (ueh != null) {
                ueh.uncaughtException(t, e);
            } else if (!(e instanceof ThreadDeath)) {
              	//可以看到会打到System.err里面
                System.err.print("Exception in thread \""
                                 + t.getName() + "\" ");
                e.printStackTrace(System.err);
            }
        }
    }

这里如果环境是tomcat的话最终会打到catalina.out:

_6145c123-4ec7-4856-b106-6c61e6dca285

总结

对于线程池、包括线程的异常处理推荐一下方式:

1 直接try/catch,个人 基本都是用这种方式

2 线程直接重写整个方法:

       Thread t = new Thread();
       t.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
 
           public void uncaughtException(Thread t, Throwable e) {
              LOGGER.error(t + " throws exception: " + e);
           }
        });
		//如果是线程池的模式:
        ExecutorService threadPool = Executors.newFixedThreadPool(1, r -> {
            Thread t = new Thread(r);
            t.setUncaughtExceptionHandler(
                (t1, e) -> LOGGER.error(t1 + " throws exception: " + e));
            return t;
        });

3 也可以直接重写protected void afterExecute(Runnable r, Throwable t) { }方法

Flag Counter

Etcd Raft使用入门及原理解析

什么是Raft

Raft是一个分布式一致性算法,充分的利用了可复制状态机以及日志。其最核心的设计目标就是易于理解。在性能、错误容错等方面来看有点类似Paxos,但不同之处在于,Raft论文较为清晰的描述了其主要流程以及其中一些细节问题,而Paxos我们知道非常难以理解。

当构建一个分布式系统时,一个非常重要的设计目标就是fault tolerance。如果系统基于Raft协议实现,那么当其中一个节点挂掉,或者发生了网络分区等异常情况时,只要大多数节点仍然能够正常通讯,整个集群就能够正常对外提供服务而不会挂掉。

关于Raft更多的细节,这里建议直接阅读论文: "In Search of an Understandable Consensus Algorithm"

介绍

Etcd的Raft库已经在生产环境得到了非常广泛的应用,有力的支撑了etcd、K8S、Docker Swarm、TiDB/TiKV等分布式系统的构建,当你能够熟练的使用一个成熟的Raft库、甚至如果能够自己实现一个,那会有种'有了锤子,干什么都是钉子'的感觉。

特性

Etcd raft基本上已经实现了Raft协议的完整特性,包括:

  • Leader选举
  • 日志复制
  • 日志压缩
  • 成员变更
  • Leader和Follower都支持高效的线性只读查询请求
  • 通过batch、pipeline等手段优化日志复制、网络IO的延迟

概览

etcd的raft实现都在etcd/raft目录下,但是大部分的实现都在下面几个比较核心的文件:

  • raft.go: 从名字也可以看出来,这个是最核心的部分,比如leader选择的逻辑、raft消息的处理逻辑等
  • node.go: 可以理解为raft集群的一个节点,客户端也主要是这个类打交道,比如心跳的逻辑、propose、状态机、成员变更等都是这个类负责处理。
  • log.go: raft日志相关的代码,比如保存日志记录
  • raft.proto: 定义了raft一些核心的RPC数据结构,由于protobuf是跨语言的,因此如果想用其他语言重写etcd raft,那么至少这部分内容都是可以复用的

用法

客户端主要使用Node和raft集群交互,首先需要启动一个raft集群,有两种方式:

  • 启动一个全新的raft集群
  • 加入一个已经存在的raft集群(节点重启、扩容、缩容)

启动一个三节点的集群:

 storage := raft.NewMemoryStorage()
  c := &Config{
    //代表一个节点的ID,必须唯一,并且不能为0,不能重复利用,和zookeeper的id类似
    ID:              0x01, 
    ElectionTick:    10, 
    HeartbeatTick:   1,
    Storage:         storage,
    MaxSizePerMsg:   4096,
    MaxInflightMsgs: 256,
  }

 //设置节点列表
  n := raft.StartNode(c, []raft.Peer{{ID: 0x02}, {ID: 0x03}})

这里需要强调一个点,etcd的raft实现并不包括网络部分,网络通讯部分需要使用者自己实现,因此这里节点列表传入的是ID,而ip:port到id的映射需要库使用者自己实现。
如果让一个新的节点加入集群,那么就不需要传入节点列表,首先通过ProposeConfChange RPC发起一个成员变更请求,在任意一个raft集群节点都可以,然后启动这个节点:

  //配置参考上文中的代码段
  n := raft.StartNode(c, nil)

如果是重启一个节点,那么这里需要注意,我们需要恢复这个节点之前的状态,比如当前term、根据快照和日志恢复状态机等:

 storage := raft.NewMemoryStorage()

  // Recover the in-memory storage from persistent snapshot, state and entries.
  // 根据快照、entry日志等恢复当前raft节点到之前的状态
  storage.ApplySnapshot(snapshot)
  storage.SetHardState(state)
  storage.Append(entries)

  c := &Config{
    ID:              0x01,
    ElectionTick:    10,
    HeartbeatTick:   1,
    Storage:         storage,
    MaxSizePerMsg:   4096,
    MaxInflightMsgs: 256,
  }

  // Restart raft without peer information.
  // Peer information is already included in the storage.
  // 重启该raft节点,此时不用传入任何节点相关信息,因为已经在刚刚的恢复过程中填充好了
  n := raft.RestartNode(c)

当raft集群启动完成后,对于一个raft节点,用户需要做几件事情,伪码如下:

for {
    select {
    case <-s.Ticker:
      n.Tick()
    case rd := <-s.Node.Ready():
      saveToStorage(rd.HardState, rd.Entries, rd.Snapshot)
      send(rd.Messages)
      if !raft.IsEmptySnap(rd.Snapshot) {
        processSnapshot(rd.Snapshot)
      }
      for _, entry := range rd.CommittedEntries {
        process(entry)
        if entry.Type == raftpb.EntryConfChange {
          var cc raftpb.ConfChange
          cc.Unmarshal(entry.Data)
          s.Node.ApplyConfChange(cc)
        }
      }
      s.Node.Advance()
    case <-s.done:
      return
    }
  }
case <-s.Ticker

库使用者需要定时调用tick()方法,根据节点当前的角色调用对应的逻辑:

  • 心跳, leader需要定时发送心跳包给follower
  • 选举,如果一定时间没有收到leader的心跳,则转换为候选者,竞选leader
case rd := <-s.Node.Ready(): 处理Ready

Ready封装了可以准备开始读取的entries、messages,需要保存到持久化介质、同步给其他节点:

type Ready struct {
	// The current volatile state of a Node.
	// SoftState will be nil if there is no update.
	// It is not required to consume or store SoftState.
	*SoftState

	// The current state of a Node to be saved to stable storage BEFORE
	// Messages are sent.
	// HardState will be equal to empty state if there is no update.
	pb.HardState

	// ReadStates can be used for node to serve linearizable read requests locally
	// when its applied index is greater than the index in ReadState.
	// Note that the readState will be returned when raft receives msgReadIndex.
	// The returned is only valid for the request that requested to read.
	ReadStates []ReadState

	// Entries specifies entries to be saved to stable storage BEFORE
	// Messages are sent.
	Entries []pb.Entry

	// Snapshot specifies the snapshot to be saved to stable storage.
	Snapshot pb.Snapshot

	// CommittedEntries specifies entries to be committed to a
	// store/state-machine. These have previously been committed to stable
	// store.
	CommittedEntries []pb.Entry

	// Messages specifies outbound messages to be sent AFTER Entries are
	// committed to stable storage.
	// If it contains a MsgSnap message, the application MUST report back to raft
	// when the snapshot has been received or has failed by calling ReportSnapshot.
	Messages []pb.Message

	// MustSync indicates whether the HardState and Entries must be synchronously
	// written to disk or if an asynchronous write is permissible.
	MustSync bool
}
  • 调用 Node.Ready(),处理当前raft节点的状态,其中有些步骤可以并行执行
    • 将entries、HardState、快照按照顺序写到持久化介质中,底层存储介质支持原子写入,那么也可以一次性将他们写入
    • 将所有的消息发送给远程节点,但一定要先将最近的HardState、上一轮Ready中的entries都持久化之后(可以和同一轮的entries持久化并行执行)。如果有类型为MsgSnap的消息,在这个消息发送成功之后,需要调用Node.ReportSnapshot()
    • 如果有快照的话需要和已提交的entries一起应用到状态机(库使用者提供),如果已经提交的entries中包含EntryConfChange,那么需要调用Node.ApplyConfChange() 将节点的变更信息同步到本节点
  • 调用Node.Advance()通知节点,表明本轮Ready已经处理完毕,可以开始处理下一轮。

另外还需要注意,由于网络部分需要库使用者自己实现,因此当收到一条消息的时候,需要将该消息转发给raft节点:

	func recvRaftRPC(ctx context.Context, m raftpb.Message) {
		n.Step(ctx, m)
	}

发起提议

如果需要向raft集群发起一个提议,那么需要用下面这种方式:

        // 协议的数据持久化成字节数组
	n.Propose(ctx, data)

如果找个提议处理完成(已经持久化到持久化介质并同步到其他节点),那么就可以通过Ready的comitedEntries获取到,类型是raftpb.EntryNormal, 然后用户就可以根据自己的业务逻辑,将其应用到状态机中。

raft集群不保证该协议一定能够处理成功,若一定超时时间内,还未收到响应,那么需要根据业务场景考虑是否需要重试。

节点变更

如果需要对raft集群扩容或缩容,那么需要构造ConfChange,并调用:

	n.ProposeConfChange(ctx, cc)

如果该变更请求处理成功,那么在commitedEntries中会有一条类型为 raftpb.EntryConfChange的记录,

        var cc raftpb.ConfChange
	cc.Unmarshal(data)
	n.ApplyConfChange(cc)

需要自己实现的部分

etcd的raft已经实现了大部分的功能,但是还是有几个组件需要使用者自己根据业务场景实现:

  • 网络通讯部分
  • Write ahead log
  • 快照

网络通讯部分

网络部分说白了就是消息的收发,你可以理解为raft只依赖了接口,这个接口实现了两个方法: sendreceive,但是具体的实现需要库使用者自己写,这部分相对比较简单,使用RPC、HTTP、自定义协议都可以,具体的实现逻辑可以参考etcd自己的代码

Write-Ahead-Log(WAL)

如上文中提到的,用户需要保存Ready中的一些状态,比如entries、hardstate等,WAL有很多分布式系统都实现了,基本上参考他们的实现,结合自己的业务实现一个难度不会很大,如果是直接使用etcd raft库,那么可以直接基于etcd中wal的实现,另外也可以基于RocksDB等嵌入式KV实现,但是对于key-value的结构设计要考虑好,wal的原理后面有时间再叙述。

快照

快照应该都知道,比如说Redis的持久化,有一种模式是保存用户发过来的命令,但时间长了之后,这个日志会变的越来越大,这个时候当你扩容、重启节点的时候,加载这个文件会耗费很长时间,导致服务不可用,因此需要将内存中的状态持久化到磁盘中。
比如:

incr index 
incr index

这个时候index的值为2,当然这个例子只有两条命令,但假如说有一千万条记录,那么重放日志需要耗费很长时间,因此我们可以直接将index:2这个kv对写到磁盘中,那么这个时候之前对这个key的一千万条操作日志就变成了这一条记录。
那么raft的快照其实也类似,应用需要将自己状态机的当前快照,持久化成一个快照文件,并写入磁盘中,我们知道这个过程会非常慢,因此可以考虑和其他过程并行执行,以及其他的一些性能优化,这个后面的博客再写。
简单来实现的话,我们直接将状态机用json序列化成一个字节数组,并写入到本地文件中,后续读取的时候。

如何基于raft实现一个简单的分布式KV存储

这里简单描述一下流程,只是为了更容易理解etcd raft的使用方法,后面会再写篇博客详细记录:

  • 应用实现自己的状态机,处理快照、已提交日志、WAL等
  • 当用户发起一个put请求时,将该请求序列化成字节数组,propose到raft集群
  • 处理成功后,会出现在commitedEntries中,解析该entry,回放到状态机中,这个时候该请求的结果已经可以在所有的raft节点上查询到了
  • 用户发起查询请求,直接在用户封装的状态机中查询,并返回给用户

总结

本文只是简单描述了下etcd raft的使用方法,总的来说etcd raft的实现已经非常完善,但还是需要用户自己处理非常多的细节,比如网络、write aheadlog等,如果对raft不熟悉,相信会很难上手,我的想法是能够在其之上再封装一层,提供一个状态机接口,用户只需要关心自己的业务逻辑,其他的全部都交给库来处理。

How to run Apache Kafka using IntelliJ IDEA

本文简单记录一下如何搭建Apache Kafka 开发环境

前言

我这里用的是MBP,如果是Windows用户那么下面的命令以及一些路径可能会有差异,比如脚本后缀应该是.bat.

安装相关库&&工具

  1. JDK1.8
  2. Gradle
  3. Zookeeper

构建Kafka

  1. https://github.com/apache/kafka, 下载源码
  2. 执行以下命令
cd kafka
gradle idea 
#这里会下载一坨依赖,慢慢等,看到Build Successful说明成功了

引入IDE

  1. 首先需要安装Scala插件
    image

  2. 配置日志,这里需要将根目录下的conf/log4j.properties拷贝到/core/src/main/resources/log4j.properties,resources文件夹不存在则新建一个

  3. 配置运行时设置,直接参考下图即可
    image

  4. 点击运行,第一次启动时Idea会先编译整个项目,因此可能会比较慢,日志如下则表示启动成功:

[2017-11-15 21:11:47,436] INFO [Transaction Marker Channel Manager 0]: Starting (kafka.coordinator.transaction.TransactionMarkerChannelManager)
[2017-11-15 21:11:47,436] INFO [TransactionCoordinator id=0] Startup complete. (kafka.coordinator.transaction.TransactionCoordinator)
[2017-11-15 21:11:47,470] INFO [/config/changes-event-process-thread]: Starting (kafka.common.ZkNodeChangeNotificationListener$ChangeEventProcessThread)
[2017-11-15 21:11:47,504] INFO Creating /brokers/ids/0 (is it secure? false) (kafka.utils.ZKCheckedEphemeral)
[2017-11-15 21:11:47,509] INFO Result of znode creation is: OK (kafka.utils.ZKCheckedEphemeral)
[2017-11-15 21:11:47,510] INFO Registered broker 0 at path /brokers/ids/0 with addresses: EndPoint(192.168.1.103,9092,ListenerName(PLAINTEXT),PLAINTEXT) (kafka.utils.ZkUtils)
[2017-11-15 21:11:47,515] WARN Error while loading kafka-version.properties :null (org.apache.kafka.common.utils.AppInfoParser)
[2017-11-15 21:11:47,516] INFO Kafka version : unknown (org.apache.kafka.common.utils.AppInfoParser)
[2017-11-15 21:11:47,516] INFO Kafka commitId : unknown (org.apache.kafka.common.utils.AppInfoParser)
[2017-11-15 21:11:47,517] INFO [KafkaServer id=0] started (kafka.server.KafkaServer)

验证

  1. 首先来创建一个topic,单个replication,单个partitions
> bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test

Created topic "test".

现在就可以看到topic已经创建成功了:

bin/kafka-topics.sh --list --zookeeper localhost:2181                                                                                                                                                                                                                !10280
test
  1. 发几个消息试试
> bin/kafka-console-producer.sh --broker-list localhost:9092 --topic test
Hello World
你好,世界!
  1. 新开一个窗口,用于消费消息:
> bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test --from-beginning
Hello World
你好,世界!

Flag Counter

日志: 分布式系统的核心

最近这段时间一直在研究消息队列、文件系统、数据库等,慢慢的发现他们都有一个核心组件:日志.有时也叫write-ahead logs 、commit logs 或者事物 logs, 通常指在应用所有的修改之前先写入日志,一般会将重放日志、撤销日志都写进去。
我们经常听到很多名词,NoSQL数据库、KV存储、Hadoop、raft、paxos 以及版本控制等等,这些中间件或者协议本质上都或多或少依赖于日志,可以发现日志一直都在分布式系统中扮演者非常重要的角色。

什么是日志?

日志就是按照时间顺序追加的、完全有序的记录序列,其实就是一种特殊的文件格式,文件是一个字节数组,而这里日志是一个记录数据,只是相对于文件来说,这里每条记录都是按照时间的相对顺序排列的,可以说日志是最简单的一种存储模型,读取一般都是从左到右,例如消息队列,一般是线性写入log文件,消费者顺序从offset开始读取。
由于日志本身固有的特性,记录从左向右开始顺序插入,也就意味着左边的记录相较于右边的记录“更老”, 也就是说我们可以不用依赖于系统时钟,这个特性对于分布式系统来说相当重要。
1511274263284

日志的应用

日志在数据库中的应用

日志是什么时候出现已经无从得知,可能是概念上来讲太简单。在数据库领域中日志更多的是用于在系统crash的时候同步数据以及索引等,例如MySQL中的redo log,redo log是一种基于磁盘的数据结构,用于在系统挂掉的时候保证数据的正确性、完整性,也叫预写日志,例如在一个事物的执行过程中,首先会写redo log,然后才会应用实际的更改,这样当系统crash后恢复时就能够根据redo log进行重放从而恢复数据(在初始化的过程中,这个时候不会还没有客户端的连接)。日志也可以用于数据库主从之间的同步,因为本质上,数据库所有的操作记录都已经写入到了日志中,我们只要将日志同步到slave,并在slave重放就能够实现主从同步,这里也可以实现很多其他需要的组件,我们可以通过订阅redo log 从而拿到数据库所有的变更,从而实现个性化的业务逻辑,例如审计、缓存同步等等。

日志在分布式系统中的应用

image
分布式系统服务本质上就是关于状态的变更,这里可以理解为状态机,两个独立的进程(不依赖于外部环境,例如系统时钟、外部接口等)给定一致的输入将会产生一致的输出并最终保持一致的状态,而日志由于其固有的顺序性并不依赖系统时钟,正好可以用来解决变更有序性的问题。
我们利用这个特性实现解决分布式系统中遇到的很多问题。例如RocketMQ中的备节点,主broker接收客户端的请求,并记录日志,然后实时同步到salve中,slave在本地重放,当master挂掉的时候,slave可以继续处理请求,例如拒绝写请求并继续处理读请求。日志中不仅仅可以记录数据,也可以直接记录操作,例如SQL语句。
image
日志是解决一致性问题的关键数据结构,日志就像是操作序列,每一条记录代表一条指令,例如应用广泛的Paxos、Raft协议,都是基于日志构建起来的一致性协议。
image

日志在Message Queue中的应用

日志可以很方便的用于处理数据之间的流入流出,每一个数据源都可以产生自己的日志,这里数据源可以来自各个方面,例如某个事件流(页面点击、缓存刷新提醒、数据库binlog变更),我们可以将日志集中存储到一个集群中,订阅者可以根据offset来读取日志的每条记录,根据每条记录中的数据、操作应用自己的变更。
这里的日志可以理解为消息队列,消息队列可以起到异步解耦、限流的作用。为什么说解耦呢?因为对于消费者、生产者来说,两个角色的职责都很清晰,就负责生产消息、消费消息,而不用关心下游、上游是谁,不管是来数据库的变更日志、某个事件也好,对于某一方来说我根本不需要关心,我只需要关注自己感兴趣的日志以及日志中的每条记录。
image

我们知道数据库的QPS是一定的,而上层应用一般可以横向扩容,这个时候如果到了双11这种请求爆发的场景,数据库会吃不消,那么我们就可以引入消息队列,将每个队数据库的操作写到日志中,由另外一个应用专门负责消费这些日志记录并应用到数据库中,而且就算数据库挂了,当恢复的时候也可以从上次消息的位置继续处理(Kafka都支持Exactly Once语义),这里即使生产者的速度异于消费者的速度也不会有影响,日志在这里起到了缓冲的作用,它可以将所有的记录存储到日志中,并定时同步到slave节点,这样消息的积压能力能够得到很好的提升,因为写日志都是有master节点处理,读请求这里分为两种,一种是tail-read,就是说消费速度能够跟得上写入速度的,这种读可以直接走缓存,而另一种也就是落后于写入请求的消费者,这种可以从slave节点读取,这样通过IO隔离以及操作系统自带的一些文件策略,例如pagecache、缓存预读等,性能可以得到很大的提升。
image

分布式系统中可横向扩展是一个相当重要的特性,加机器能解决的问题都不是问题。那么如何实现一个能够实现横向扩展的消息队列呢? 假如我们有一个单机的消息队列,随着topic数目的上升,IO、CPU、带宽等都会逐渐成为瓶颈,性能会慢慢下降,那么这里如何进行性能优化呢?

  1. topic/日志分片,本质上topic写入的消息就是日志的记录,那么随着写入的数量越多,单机会慢慢的成为瓶颈,这个时候我们可以将单个topic分为多个子topic,并将每个topic分配到不同的机器上,通过这种方式,对于那些消息量极大的topic就可以通过加机器解决,而对于一些消息量较少的可以分到到同一台机器或不进行分区
  2. group commit,例如Kafka的producer客户端,写入消息的时候,是先写入一个本地内存队列,然后将消息按照每个分区、节点汇总,进行批量提交,对于服务器端或者broker端,也可以利用这种方式,先写入pagecache,再定时刷盘,刷盘的方式可以根据业务决定,例如金融业务可能会采取同步刷盘的方式。
  3. 规避无用的数据拷贝
  4. IO隔离, 对于写请求可以直接走master节点,而来自不同客户端的读请求我们可以根据他们需要读取的内容(offset)将其分配到不同的slave节点,从而实现IO的隔离
  5. 缓存,我们可以根据当前消费者的进度,决定是否需要缓存正在写入的消息,例如BookKeeper,我们将新写入的消息同步到memory table,这样对于tailling read就可以直接走缓存,而对于"落后"的消费者我们可以让其直接读取slave的数据

image

结语

日志在分布式系统中扮演了很重要的角色,是理解分布式系统各个组件的关键,随着理解的深入,我们发现很多分布式中间件都是基于日志进行构建的,例如Zookeeper、HDFS、Kafka、RocketMQ、Google Spanner等等,甚至于数据库,例如Redis、MySQL等等,其master-slave都是基于日志同步的方式,依赖共享的日志系统,我们可以实现很多系统: 节点间数据同步、并发更新数据顺序问题(一致性问题)、持久性(系统crash时能够通过其他节点继续提供服务)、分布式锁服务等等,相信慢慢的通过实践、以及大量的论文阅读之后,一定会有更深层次的理解。

Flag Counter

Running Spring Boot in a Docker cluster

前言

现在的应用服务基本都是部署在云服务上,但是很多公司还是没能够充分的利用好Cloud的优势,去做到快速、甚至是自动扩容,本篇博客就简单演示一下如何Docker Swarm部署应用。

实战

创建API服务

首先我们写一个简单的API服务,直接在Idea初始化一个简单的SpringBoot应用,创建个简单的接口:

@RestController
@SpringBootApplication
public class DemoApplication {

	@GetMapping("/hello")
	public String hello() throws UnknownHostException {
		return "Hello World:" + Inet4Address.getLocalHost().getHostName();
	}

	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}
}

代码很简单,然后将端口改成9000,修改application.properties:

server.port=9000

创建Docker镜像

在项目根目录创建docker-compose.yml,注意visualizer这坨,这端会创建一个图形化界面,类似K8S自带的那个玩意,不过功能相对简单了点,

version: "3"
services:
  web:
    # 换成你自己的,或者用我已经创建好的镜像也行
    image: acoder2013/get-started:part4
    deploy:
      replicas: 5
      restart_policy:
        condition: on-failure
      resources:
        limits:
          cpus: "0.1"
          memory: 200M
    ports:
      - "9000:9000"
    networks:
      - webnet
  visualizer:
    image: dockersamples/visualizer:stable
    ports:
      - "8080:8080"
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock"
    deploy:
      placement:
        constraints: [node.role == manager]
    networks:
      - webnet
networks:
  webnet:

下面执行命令,创建镜像:

mvn clean package -Dmaven.test.skip=true
docker build -t demo8  .
docker tag demo8 acoder2013/get-started:part4
docker push acoder2013/get-started:part4

然后等待上传完成:

± % docker push acoder2013/get-started:part4                                                                                                                                                                                                                  

The push refers to a repository [docker.io/acoder2013/get-started]

f879b4a743a5: Preparing 
9e47d8741070: Preparing 
35c20f26d188: Preparing 
c3fe59dd9556: Preparing 
6ed1a81ba5b6: Preparing 
a3483ce177ce: Waiting 
ce6c8756685b: Waiting 
30339f20ced0: Waiting 
0eb22bfb707d: Waiting 
a2ae92ffcd29: Preparing 

搭建Docker集群

由于网络原因,我这里直接购买两台1G内存的VPS,你也可以在本机操作,首先搭建Docker Swarm集群:

docker swarm init

Swarm initialized: current node <node ID> is now a manager.

To add a worker to this swarm, run the following command:

  docker swarm join \
  --token <token> \
  <myvm ip>:<port>
To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

然后直接将这行命令拷贝到另一台机器上,接下来就开始实际部署服务:

#这个就是刚才编辑的那个文件
docker stack deploy -c docker-compose.yml getstarteddemo
root@docker_test:~# docker service ls
ID                  NAME                       MODE                REPLICAS            IMAGE                             PORTS
qksmgjhs3byc        getstartedlab_visualizer   replicated          1/1                 dockersamples/visualizer:stable   *:8080->8080/tcp
v34wty44x6hs        getstartedlab_web          replicated          5/5                 acoder2013/get-started:part4      *:9000->9000/tcp

等待几分钟服务会启动完成,然后根据VPS的外网IP地址访问,比如刚才那个图形化界面就是8080端口:
image

然后我们验证一下刚才的接口是否符合预期输出:

± % while true;do curl http://<IP>:9000/hello ; sleep 1;done;                                                                                                                                                                                                  
Hello World:9161003540aaHello World:347a853b3b50Hello World:13149fae1120Hello World:bd43db19e164Hello World:ae59a448614eHello World:9161003540aa^C%

可以观察到,每个hostname都不一样,因为这里有一个负载均衡,会在我们设置的5台之间根据一定的策略转发。

性能

虽然程序相当简单,这里也贴一个profile图
image

总结

总体来说,相对于传统的模式要方便很多,支持直接在配置文件中指定服务需要的资源,防止机器被一个应用拖垮,另外启动、扩容、缩容也支持命令直接操作,不用想像以前一样ssh到指定的服务器,而且容易出错。

Flag Counter

RocketMQ源码学习-Producer启动流程

Apache RocketMQ is a distributed messaging and streaming platform with low latency, high performance and reliability, trillion-level capacity and flexible scalability.

前言

最近正好在研究RocketMQ,因此打算写一些相关的博文,这是第一篇关于Producer的博客

Producer的启动流程

一个简单的Demo

/*
* 指定一个全局唯一的Group
*/
DefaultMQProducer producer = new DefaultMQProducer("hello-group");
/*
* 指定name server的地址,可以是多个,分号分隔
*/
producer.setNamesrvAddr("localhost:9876");
/*
* 启动
*/
producer.start();

内部如何工作?

接下来我们就尝试搞清楚上面那坨代码具体都干了哪些东西:

    /*
    * 构造函数
    */
    public DefaultMQProducer(final String producerGroup, RPCHook rpcHook) {
        this.producerGroup = producerGroup;
        defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook);
    }

    public void start() throws MQClientException {
        //将所有的调用都交给这个哥们去做
        this.defaultMQProducerImpl.start();
    }

可以看到DefaultMQProducer只是一个门面类,具体的实现都是由DefaultMQProducerImpl去做的:

    //默认的状态,是否可以用volatile修饰?
    private ServiceState serviceState = ServiceState.CREATE_JUST;

    public void start() throws MQClientException {
        this.start(true);
    }

    public void start(final boolean startFactory) throws MQClientException {
        switch (this.serviceState) {
            case CREATE_JUST:
                this.serviceState = ServiceState.START_FAILED;
                
                /*
                * 检查group的名字,不能为空也不能是默认的,因为需要全局唯一
                */
                this.checkConfig();

                if (!this.defaultMQProducer.getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP)) {
                    //如果实例名为空的话就改成进程的ID
                    this.defaultMQProducer.changeInstanceNameToPID();
                }

                //创建`MQClientFactory`实例(保存在一个map中,key的形式类似IP@进程ID)
                this.mQClientFactory = MQClientManager.getInstance().getAndCreateMQClientInstance(this.defaultMQProducer, rpcHook);

                /*
                *   放到缓存中,组名作为Key
                *   ConcurrentMap<String, MQProducerInner> producerTable
                */
                boolean registerOK = mQClientFactory.registerProducer(this.defaultMQProducer.getProducerGroup(), this);
                if (!registerOK) {
                    /*
                    *  如果组名或者Producer为空的话就会返回false,但这里不会发生;另外内部是用ConcurrentHashMap#putIfAbsent实现的,如果
                    *  返回的值非空,说明已经创建过,那么这里也会返回false,也就避免了并发启动的问题
                    */
                    this.serviceState = ServiceState.CREATE_JUST;
                    throw new MQClientException("The producer group[" + this.defaultMQProducer.getProducerGroup()
                        + "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL),
                        null);
                }
                
                /*
                * 测试用,这里缓存的结构是`ConcurrentMap<String, TopicPublishInfo>`,key是topic,也就是在这里会缓存topic的路由信息,
                * 发送消息的时候也就会根据`TopicPublishInfo`的信息决定实际使用哪个queue发送
                */
                this.topicPublishInfoTable.put(this.defaultMQProducer.getCreateTopicKey(), new TopicPublishInfo());

                if (startFactory) {
                    //启动MQClientFactory
                    mQClientFactory.start();
                }

                log.info("the producer [{}] start OK. sendMessageWithVIPChannel={}", this.defaultMQProducer.getProducerGroup(),
                    this.defaultMQProducer.isSendMessageWithVIPChannel());
                //标记为运行中
                this.serviceState = ServiceState.RUNNING;
                break;
            case RUNNING:
            case START_FAILED:
            case SHUTDOWN_ALREADY:
                throw new MQClientException("The producer service state not OK, maybe started once, "
                    + this.serviceState
                    + FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK),
                    null);
            default:
                break;
        }

        //向所有的broker发送心跳(组名)
        this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
    }

这段代码非常的清晰,其中有一行比较核心的,大部分初始化的工作都是这里完成的mQClientFactory.start();,因此我们看看这里具体都是啥:

public void start() throws MQClientException {

        //用synchronized修饰保证线程安全性与内存可见性
        synchronized (this) {
            switch (this.serviceState) {
                case CREATE_JUST:
                    this.serviceState = ServiceState.START_FAILED;
                    // 如果未指定的话就会通过制定的接口去获取name server的地址,超时时间是3秒
                    if (null == this.clientConfig.getNamesrvAddr()) {
                        this.mQClientAPIImpl.fetchNameServerAddr();
                    }
                    /* 
                    * 启动用于通讯的客户端,内部是用Netty实现的
                    */
                    this.mQClientAPIImpl.start();
                    // 启动所有的定时任务
                    this.startScheduledTask();
                    // TODO:目前还不清楚为啥生产者还需要启动一个线程专门用于拉消息
                    this.pullMessageService.start();
                    // 启动均衡消息的线程
                    this.rebalanceService.start();
                    // 启动它内部的Producer
                    this.defaultMQProducer.getDefaultMQProducerImpl().start(false);
                    log.info("the client factory [{}] start OK", this.clientId);
                    this.serviceState = ServiceState.RUNNING;
                    break;
                case RUNNING:
                    break;
                case SHUTDOWN_ALREADY:
                    break;
                case START_FAILED:
                    throw new MQClientException("The Factory object[" + this.getClientId() + "] has been created before, and failed.", null);
                default:
                    break;
            }
        }
}

private void startScheduledTask() {
        if (null == this.clientConfig.getNamesrvAddr()) {
            this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

                @Override
                public void run() {
                    try {
                        /*
                        * 每两分钟抓取一次,也就是说可以通过这个服务定时摘掉挂了的broker,和心跳检测
                        * 双重保障
                        */
                        MQClientInstance.this.mQClientAPIImpl.fetchNameServerAddr();
                    } catch (Exception e) {
                        log.error("ScheduledTask fetchNameServerAddr exception", e);
                    }
                }
            }, 1000 * 10, 1000 * 60 * 2, TimeUnit.MILLISECONDS);
        }

        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

            @Override
            public void run() {
                try {
                    //定时更新Topic的路由信息
                    MQClientInstance.this.updateTopicRouteInfoFromNameServer();
                } catch (Exception e) {
                    log.error("ScheduledTask updateTopicRouteInfoFromNameServer exception", e);
                }
            }
        }, 10, this.clientConfig.getPollNameServerInterval(), TimeUnit.MILLISECONDS);

        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

            @Override
            public void run() {
                try {
                    //健康检查相关的
                    MQClientInstance.this.cleanOfflineBroker();
                    MQClientInstance.this.sendHeartbeatToAllBrokerWithLock();
                } catch (Exception e) {
                    log.error("ScheduledTask sendHeartbeatToAllBroker exception", e);
                }
            }
        }, 1000, this.clientConfig.getHeartbeatBrokerInterval(), TimeUnit.MILLISECONDS);

        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

            @Override
            public void run() {
                try {
                    /*
                    * 持久化消费者当前消费的位移,这里说一下消费者的可能会踩到的坑,
                    * 对于同一个topic,不同group下的消费者offset是独立的,也就是
                    * 同一个消息会消费两次
                    */
                    MQClientInstance.this.persistAllConsumerOffset();
                } catch (Exception e) {
                    log.error("ScheduledTask persistAllConsumerOffset exception", e);
                }
            }
        }, 1000 * 10, this.clientConfig.getPersistConsumerOffsetInterval(), TimeUnit.MILLISECONDS);

        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

            @Override
            public void run() {
                try {
                    /*
                    *  根据当前的积压调优线程池的核心线程数,不过看了下实现是空的
                    */
                    MQClientInstance.this.adjustThreadPool();
                } catch (Exception e) {
                    log.error("ScheduledTask adjustThreadPool exception", e);
                }
            }
        }, 1, 1, TimeUnit.MINUTES);
}

最后来一张流程图:

_ae6a3eb4-19cc-4a29-8ba3-759e5e0ce037

Flag Counter

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.