微服务之并发与异步

并发

顺序执行在大多数情况下都挺好的, 简单明了, 一个时间专心做一件事, 不容易出错.
但是在多核时代, 追求更高更快更强, 应对复杂的计算和逻辑处理, 并发是不二法门.

这方面的经典书籍有两本我很喜欢

1. Pattern-Oriented Software Architecture Volume 2: Patterns for Concurrent and Networked Objects Volume 2 Edition
by Douglas Schmidt (Author), Michael Stal (Author), Hans Rohnert (Author), Frank Buschmann (Author)

1598924-046e5fefd9251641.png
POSA2

2. Java Concurrency in Practice 1st Edition
by Brian Goetz (Author), Tim Peierls (Author), Joshua Bloch (Author), Joseph Bowbeer (Author), David Holmes (Author), Doug Lea (Author)

1598924-9e6a42aaa918dc26.png
jcip

所谓并发, 这里主要讲的是多线程编程, 当然多进程, 分布式多服务器也是并发编程的范畴, 这方面的东西以后再说

从 Java 语言的角度来看, Java 提供了基础的多线程库

Thread 早期是JVM中的一个分时调用的所谓绿色线程, 并不是真正的线程, 现在的版本多是一一对应于系统的 pthread , 线程的优先级从1到10, MAX_PRIORITY是最高优先级.

Java线程分为守护线程(thread.setDaemon(true))和非守护线程, 应该避免直接调用 thread 的 stop, suspend 和 resume , 这些方法并不可靠

Java 5 是一个分水岭, 它提供了一个非常好用的 concurrent 包, 有

  • 线程执行服务 ExecutorService
  • 线程安全容器 ConcurrentHashMap 等
  • 阻塞队列 BlockingQueue
  • 信号量 Semaphore
  • 屏障 CyclicBarrier
  • 倒数计量锁 CountDownLatch
  • 相位器 Phaser
  • 交换器 Exechanger

Java 7 提供了 Fork/Join 框架

Java 8 又是一个里程碑, 它提供了 并行流和 ComparableFuture

举例如下:
比如我们有一个图书馆系统,提供按照书名, 作者和出版日期查询的API。

现在我想查一本书, 它是本和 java 语言有关的书, 大约是2005年之后出的书,依稀记得作者是 "Brian Goetz", "Tim Peierls", "Joshua Bloch", "Joseph Bowbeer", "David Holmes" 或 "Doug Lea" 这几个人, 由于系统所提供 API 的限制,我可以按人名一个一个顺序查找,直到找到结果.

也可以并发查找,要求按照人名顺序为优先级取结果.

让我们看看具体实现和查询结果

package com.github.walterfan.example.concurrent;

import com.google.common.base.Stopwatch;
import com.google.common.util.concurrent.Uninterruptibles;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import java.util.stream.Collectors;

/**
 * Created by walter on 15/04/2017.
 */
public class FutureTest {
    public static final String AUTHOR_NAME = "Joseph Bowbeer";
    private AtomicInteger counter = new AtomicInteger(0);
    private Logger logger = LoggerFactory.getLogger(FutureTest.class);


    private ExecutorService pool;

    private String[] authors  = new String[] {
            "Brian Goetz",
            "Tim Peierls",
            "Joshua Bloch",
            "Joseph Bowbeer",
            "David Holmes",
            "Doug Lea" };

    private Instant earliestDate = Instant.parse("2005-12-31T00:00:00.00Z");

    public static class Book {
        final String title;
        final String author;
        final String isbn;
        final Instant publicationDate;

        public Book(String title, String author, String isbn, Instant publicationDate) {
            this.title = title;
            this.author = author;
            this.isbn = isbn;
            this.publicationDate = publicationDate;
        }

        @Override
        public String toString() {
            return "Book{" +
                    "title='" + title + '\'' +
                    ", author='" + author + '\'' +
                    ", isbn='" + isbn + '\'' +
                    ", publicationDate=" + publicationDate +
                    '}';
        }
    }

    public Book queryBook(String title, String author, Instant earliestDate) {
        int num = counter.incrementAndGet();
        int ms = 80;

        logger.info("{}. query book, times  {} " , num, author);

        if("Joshua Bloch".equals(author)) {
            ms = 120;
        }
        Uninterruptibles.sleepUninterruptibly(ms, TimeUnit.MILLISECONDS);

        if(Arrays.asList(new String[]{"Brian Goetz","Tim Peierls","Joshua Bloch"}).contains(author)) {
            return null;
        }

        return new Book("Java Concurrency in Practice", author, String.valueOf(num), Instant.parse("2006-05-19T00:00:00.00Z"));
    }



    @BeforeClass
    public void init() {
        int corePoolSize = 4;
        int maxPoolSize = 16;
        long keepAliveTime = 5000;
        int queueCapacity = 200;

        pool = new ThreadPoolExecutor(
                corePoolSize,
                maxPoolSize,
                keepAliveTime,
                TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>(queueCapacity)
        );
    }

    @AfterClass
    public void clean() {
        if(null == pool) {
            return;
        }

        try {
            logger.info("attempt to shutdown executor");
            pool.shutdown();
            pool.awaitTermination(2, TimeUnit.SECONDS);
        }
        catch (InterruptedException e) {
            logger.error("tasks interrupted");
        }
        finally {
            if (!pool.isTerminated()) {
                logger.error("cancel non-finished tasks");
            }
            pool.shutdownNow();
            logger.info("shutdown finished");
        }
    }


    public Optional<Book> querySequentially() {

        for (String author : authors) {
            Optional<Book> book = Optional.ofNullable(queryBook("java", author, earliestDate));
            if (book.isPresent()) {
                return book;
            }
        }
        return Optional.empty();

    }

    public Optional<Book> queryConcurrently ()  {
        List<Future<Book>> futureBooks = new ArrayList<>(authors.length);
        for (String author : authors) {
            futureBooks.add(pool.submit(() -> queryBook("java", author, earliestDate)));
        }

        Optional<Book> ret = null;
        for (Future<Book> futureBook : futureBooks) {
            try {
                ret = Optional.ofNullable(futureBook.get(100, TimeUnit.MILLISECONDS));
            } catch (TimeoutException |InterruptedException|ExecutionException e) {
                continue;
            }
            if (ret.isPresent()) {
                break;
            }
        }
        return ret;
    }

    public Optional<Book> queryParallelly ()  {
        List<Optional<Book>> books = Arrays.asList(authors).parallelStream()
                .map(x -> queryBook("java", x, earliestDate))
                .map(x -> Optional.ofNullable(x))
                .collect(Collectors.toList());

        //books.stream().filter(x -> x.isPresent()).forEach(x -> logger.info(x.get().toString()));
        for(String author: authors) {

            Optional<Book> opt = books.stream().filter(x -> x.isPresent()).map(x -> x.get())
                    .filter(x -> author.equals(x.author)).findFirst();

            if(opt.isPresent())
                return opt;

        }

        return Optional.empty();
    }

    @Test
    public void testQuery() {


        logger.info("-- querySequentially ---");
        long durationSequentially  = recordExecutionTime(() -> querySequentially());

        logger.info("-- queryConcurrently ---");
        long durationConcurrently  = recordExecutionTime( () -> queryConcurrently());

        logger.info("-- queryParallelly ---");
        long durationParallelly  = recordExecutionTime(() -> queryParallelly());

        logger.info("duration: querySequentially={} > queryParallelly={} > queryConcurrently={},", durationSequentially, durationParallelly, durationConcurrently );
        Assert.assertTrue(durationSequentially > durationConcurrently
                && durationSequentially > durationConcurrently
                && durationParallelly > durationConcurrently);
    }


    public long recordExecutionTime(Supplier<Optional<Book>> supplier)  {
        counter.set(0);
        final Stopwatch stopwatch = Stopwatch.createStarted();

        Optional<Book> book = supplier.get();


        stopwatch.stop();

        long duration = stopwatch.elapsed(TimeUnit.MILLISECONDS);

        Assert.assertEquals(book.map(x -> x.author).orElse(null), AUTHOR_NAME);
        return duration;

    }


}

执行结果如下:

duration: querySequentially=380 > queryParallelly=225 > queryConcurrently=137

当然是顺序执行耗时最长, queryParallelly 并行流多等待了一会儿, 所以是queryConcurrently 方法效率最高

线程池的线程数不宜过多,以免造成对于cpu和内存资源的竞争,频繁的上下文切换,也不宜过少,以免影响性能,白白闲置cpu资源,Brian Goetz 有个公式可以参考

线程数 = CPU 个数 * CPU 利用率 * (等待时间/计算时间 + 1)

IO 密集型的应用,等待时间较多,线程数可以适当增加点
而计算密集型,耗费CPU 本来就多,线程数适可而止,不宜过多

比如4核CPU, CPU 利用率为0.5(50%), 大约80%的时间在等待API 的响应

4*0.5*(80/20+1)=10 个线程

异步

异步是相对于同步来说的, 你可以一步一步以顺序执行的方式写出异步程序, 关键在于你是否会等待每一步的执行结果, 再执行下一步. 要知道, 等待是很浪费时间的, 与其苦苦等待, 不如在把用来等待的时间做点更有意义的事.

代码层面

异步其实分为多个层面, 类似上述并发所提到的, 有代码层面的异步方式, 如进程, 线程和协程.
拿最常用的写日志来说, 同步方法比较简单, 把日志写到磁盘上再返回, 最早的 log4j 也就是这种方法, 如果你需要写大量的日志, 这种方法对性能的损耗是比较大的, 毕竟写磁盘比较慢, 即使是追加的方式也不可忽视.

Log4j 中使用异步log , 会提升 6~68倍的吞吐量, Log4j1 中的 Async appender 使用 ArrayBlockingQueue 和单独的 log 线程来实现异步, 应用线程把日志写到 ArrayBlockingQueue 即返回, 而单独的 log 线程把日志保存到日志文件中, log4j2 中使用了一个无锁化的线程间通信库 LMAX Disruptor , 减少了锁等待时间, 据说性能又有了大幅提升

上面提到的 Future 和 ComparableFuture 是常用的方法, 其实 future, promise, delay, 和 deferred 都是在一些编程语言中表示在并发执行中的未知结果的代理对象, 因为实际的结果可能尚未执行和计算得出, 可以从这个代理对象中查询到执行的结果.

  • Future: 表示一个异步计算的结果
  • ComparableFuture: 一个可以显式完成的异步计算结果, 有一个 CompletionStage , 支持在完成时触发相应的函数和动作, 也就是我们常说的 Promise

示例如下

package com.github.walterfan.hellotest;

import com.google.common.util.concurrent.Uninterruptibles;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.testng.annotations.Test;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;


@Slf4j
@Data
class Caller {

    private String phoneNumber;

    public Executor executor = Executors.newFixedThreadPool(10);

    public void onSuccess(String resp) {
        log.info("call success, resp is {}", resp);
    }

    public Void onError(Throwable exp) {
        log.info("error: ", exp);
        return null;
    }

    public void writeMetrics(Void nothing) {
        log.info("metrics: sent call request");
    }

    public String sendReqeust() {
        log.info("send request to call  {}", phoneNumber);
        Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS);
        return "success";

    }
}

@Slf4j
public class CompletableFutureTest {


    @Test
    public void testFuture() {


        Caller caller = new Caller();
        caller.setPhoneNumber("13011166888");

        CompletableFuture<String>  promise = CompletableFuture.supplyAsync(caller::sendReqeust);
        promise.thenAccept(strResp -> caller.onSuccess(strResp))
                .exceptionally(exp ->  caller.onError(exp) )
                .thenAccept(caller::writeMetrics);
        log.info("need not wait here, just for test to sleep 2 seconds");

        Uninterruptibles.sleepUninterruptibly(2, TimeUnit.SECONDS);
    }
}

API 层面

在 API 层面, 相比传统的请求-响应的模式, 不是等待任务完成才回复响应 200 OK, 而是立即响应 202 Accept, 之后在任务进行过程中或完成之后发送若干通知, 这样客户端和服务器端都无须浪费时间等待, 例如以下流程

1598924-5b40e12409eceda7.png
Client->Server: POST /tasks (notifyUrl)
Server-->Client: 202 Accept (taskId)
Server->Client: POST notifyUrl(started)
Client-->Server: 200 OK
opt client query server
    Client->Server: GET /tasks/taskId
    Server-->Client: 200 OK (in-progress)
end

Server->Client: POST notifyUrl(finished)
Client-->Server: 200 OK

这里的 Client 和 Server 也可以是 Service1 和 Service2, 如果 Client 是Web App, 那么也可以由 Client 主动以 taskId 来查询, 抑或以 websocket , BOSH(Bidirectional-streams Over Synchronous HTTP) 之类的双工通信方式进行主动通知.

Webhook

Webhook 的做法也是异步的一种方式, 它是一种用户定义的 HTTP 回调, 它和我们在写代码中设置一个回调函数的方式是一样一样的, 在发送请求和回复响应时都可以给定一个预定义的URL, 按照约定的调用方式在某种条件下或某个事件发生时触发这个 Webhook 回调.

实际应用中, 既可以用同步的方式使用它, 比如在持续集成系统 Jenkins 中我们可以定义一些 webhook, 例如在关键步骤时需要负责人批准, 我们可以定义一个 webhook , 在执行下一步关键步骤时调用此 webhook, 通过 IM 向负责人发送一个 url, 负责人点击这个 url 会出现一个表单, 包含同意和拒绝两个按钮, 点击同意则回调 Jenkins 继续下一步, 否则停止执行下一步.

Github 的 Pull Request 也算是用异步的方式在某人创建 PR 时发送邮件来通知相关者来审查代码, 而创建 PR 者不会等待这个异步的通知

Pubsub

订阅/发布( subscribe/publish) 模式也是如此, 这个模式如此简单又如此有用, 这里的异步体现在生产者不等消费者来取走产品, 把生产出的内容消息发到消息队列服务器 (MQ Broker) 上就好了, 而消费者也不会等着生产者, 只需订阅关心的主题, 所订阅的内容会自动推送给我

1598924-478d024ffececcc6.png
Pub-Sub Pattern (image credit: [MSDN blog])

银行的排号机是一个程序员可以借鉴的一个非常好的参照物, 它既是一个发布订阅系统, 也是一个流量控制系统, 有关发布订阅模式与消息队列系统, 内容很多, 这里不做赘述, 另外写字详细阐述.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值