blog
blog copied to clipboard
个人博客,记录个人总结(见issues)
# 背景 目前的网关是基于`Spring Boot 1.5.x` 和`Tomcat 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模型,但由于业务线程是同步处理的的,因此当并发比较高时,很容易造成线程池被打满。  - 容易受网络、磁盘IO等延迟影响。需要谨慎设置超时时间,如果设置不当,且接口之前的隔离做的不是很完善,则服务很容易被一个延迟的接口拖垮。 而异步化的方式则完全不同,通常情况下一个CPU核启动一个线程即可处理所有的请求、响应。一个请求的生命周期不再固定于一个线程,而是会分成不同的阶段交由不同的线程池处理,系统的资源能够得到更充分的利用。而且因为线程不再被某一个连接独占,一个连接所占用的系统资源也会低得多,只是一个文件描述符加上几个监听器,而在阻塞模型中,每条连接都会独占一个线程,是一个非常重的资源。对于上游服务的延迟情况,能够得到很大的缓解,因为在阻塞模型中,慢请求会独占一个线程资源,而异步化之后,因为单条连接诶所占用的资源变的非常低,因此系统可以同时处理大量的请求。 因此考虑对网关进行异步化改造,解决当前遇到的超时、延迟等问题。 ## 技术选型 #### Zuul 2 Zuul 2基于Netty和RxJava实现,采用了异步非阻塞模型,本质上其实就是队列+事件驱动。在zuul...
## 问题 客户端如何知道某一个服务的可用节点列表? ## 要求 - 每个服务的实例都会在一个特定的地址(ip:port)暴露一系列远程接口,比如HTTP/REST、RPC等 - 服务的实例以及其地址会动态变更(虚拟机或Docker容器的ip地址都是动态分配的) ## 解决方案 ### 负载均衡器 类似Nginx这类负载均衡器貌似可以解决这个问题,但是只支持静态配置,当我们对服务动态扩容、缩容时,需要联系运维进行对应的配置变更,而且如果你的服务运行在Docker或K8S时,节点的IP都是动态分配的,这时再通过Nginx去做服务发现会变的非常麻烦。另外引入一个中间层,就引入了一个潜在的故障点,虽然Nginx的性能很高,但多经过一层必然会造成一定的性能损耗。 ```nginx server { location / { proxy_pass http://localhost:8080; } location /images/ { root /data; } }...
> 并发编程的概念并不新鲜,每一种编程语言中都内置了相关的支持,而有些编程语言因为对并发提供了更有友好的支持而得到了更多的关注。 # 拥抱并发 使用并发编程并不仅仅是为了CPU多核从而使得程序能够并行执行,其本质其实就是为了消除延迟,例如访问硬盘、网络IO等慢速的设备相对单纯的CPU计算会有很高的延迟,进而导致线程阻塞在这里等待资源,这个时候CPU的资源就白白浪费了,因此我们会根据业务场景,选择开启多个线程,将这些比较耗时的IO任务丢到另外的线程中去处理,这样就不会因为某些慢请求而影响其他用户,从而提高响应时间。因此这里就涉及到了并发模型的选型,下面我选择几种并尝试总结其优劣。 ## Java Future Java [Future](http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Future.html) 代表一种异步计算的结果,调用方可以检查计算是否完成、等待计算完成、获取计算结果等,使用起来非常的直观,但当多个Future组合起来时,特别每个异步计算的耗时都不一样,代码通常会变的很复杂并且很容易出错。各种`Future#get`和`try/catch`充斥在代码中,导致可读性变的非常差。 ```java private static final ExecutorService executorService = Executors.newCachedThreadPool(); public static void main(String[] args) { Future blogsFuture = fetchBlogs(); try...
## 什么是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选举 - 日志复制 - 日志压缩 - 成员变更...
> 最近这段时间一直在研究消息队列、文件系统、数据库等,慢慢的发现他们都有一个核心组件:日志.有时也叫write-ahead logs 、commit logs 或者事物 logs, 通常指在应用所有的修改之前先写入日志,一般会将重放日志、撤销日志都写进去。 我们经常听到很多名词,NoSQL数据库、KV存储、Hadoop、raft、paxos 以及版本控制等等,这些中间件或者协议本质上都或多或少依赖于日志,可以发现日志一直都在分布式系统中扮演者非常重要的角色。 # 什么是日志? 日志就是按照时间顺序追加的、完全有序的记录序列,其实就是一种特殊的文件格式,文件是一个字节数组,而这里日志是一个记录数据,只是相对于文件来说,这里每条记录都是按照时间的相对顺序排列的,可以说日志是最简单的一种存储模型,读取一般都是从左到右,例如消息队列,一般是线性写入log文件,消费者顺序从offset开始读取。 由于日志本身固有的特性,记录从左向右开始顺序插入,也就意味着左边的记录相较于右边的记录“更老”, 也就是说我们可以不用依赖于系统时钟,这个特性对于分布式系统来说相当重要。   # 日志的应用 ## 日志在数据库中的应用 日志是什么时候出现已经无从得知,可能是概念上来讲太简单。在数据库领域中日志更多的是用于在系统crash的时候同步数据以及索引等,例如MySQL中的redo log,redo log是一种基于磁盘的数据结构,用于在系统挂掉的时候保证数据的正确性、完整性,也叫预写日志,例如在一个事物的执行过程中,首先会写redo log,然后才会应用实际的更改,这样当系统crash后恢复时就能够根据redo log进行重放从而恢复数据(在初始化的过程中,这个时候不会还没有客户端的连接)。日志也可以用于数据库主从之间的同步,因为本质上,数据库所有的操作记录都已经写入到了日志中,我们只要将日志同步到slave,并在slave重放就能够实现主从同步,这里也可以实现很多其他需要的组件,我们可以通过订阅redo log 从而拿到数据库所有的变更,从而实现个性化的业务逻辑,例如审计、缓存同步等等。 ## 日志在分布式系统中的应用...
# 前言 本文主要介绍一下Kotlin是如何实现Coroutine的,对于具体的用法推荐参考一下官方文档,讲得还是比较详细的 ## 什么是 Coroutine 概念上来说类似于线程,拥有自己的栈、本地变量、指令指针等,需要一段代码块来运行并且拥有类似的生命周期。但是和线程不同,coroutine并不和某一个特定的线程绑定,它可以在线程A中执行,并在某一个时刻暂停(suspend),等下次调度到恢复执行的时候在线程B中执行。不同于线程,coroutine是协作式的,即子程序可以通过在函数中有不同的入口点来实现暂停、恢复,从而让出线程资源。 ## 实战演练 首先看一个简单的小demo,来看看Kotlin的Coroutine是具体适合使用的: ```kotlin @Test fun async() { async { delay(1000) print("World!") } print("Hello ") Thread.sleep(2000) } ``` 上面这段代码会输出`Hello World!`, 那么下面我们看看具体是如何工作的. ## 原理剖析...
# 引言 在Java中,使用线程池来异步执行一些耗时任务是非常常见的操作。最初我们一般都是直接使用`new Thread().start`的方式,但我们知道,线程的创建和销毁都会耗费大量的资源,关于线程可以参考之前的一片博客[Java线程那点事儿](https://github.com/aCoder2013/blog/issues/4), 因此我们需要重用线程资源。 > 当然也有其他待解决方案,比如说coroutine, 目前Kotlin已经支持了,JDK也已经有了相关的提案:[Project Loom](http://openjdk.java.net/projects/loom/), 目前的实现方式和Kotlin有点类似,都是基于ForkJoinPool,当然目前还有很多限制,以及问题没解决,比如synchronized还是锁住当前线程等。 ## 继承结构  继承结构看起来很清晰,最顶层的Executor只提供了一个最简单的`void execute(Runnable command)`方法,然后是ExecutorService,ExecutorService提供了一些管理相关的方法,例如关闭、判断当前线程池的状态等,另外不同于`Executor#execute`,ExecutorService提供了一系列方法,可以将任务包装成一个Future,从而使得任务提交方可以跟踪任务的状态。而父类AbstractExecutorService则提供了一些默认的实现。 # 构造器 ThreadPoolExecutor的构造器提供了非常多的参数,每一个参数都非常的重要,一不小心就容易踩坑,因此设置的时候,你必须要知道自己在干什么。 ```java public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue...
## 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单元由若干个集群组成,单元内的集群可以互相之前复制数据,...
# 前言 消息队列允许应用之间通过发消息的方式异步通讯,简单来说,发送者和消费者的生产效率通常是不一致的,那么我们就需要一种抽象模型去解耦,因此这里就可以引入消息队列,将任务暂时写入消息中间件,待消费者慢慢处理。消息中间件目前已经有了很多选择,例如RocketMQ、Kafka、Pulsar等等,Message queue带来很多便利的同时,也引入了一些技术上的复杂性,就像一个黑盒子一样,如果不能理解其原理,碰到了问题查起来也很蛋疼,今天我们就来看看如何着手实现一个简单的消息队列 # 正文 首先我们看看Kafka以及RocketMQ的包结构,看看一个分布式消息队列究竟需要哪些组件   ## 存储层 消息队列最核心的组件之一就是存储层,消息如何落地、如何读取,这里的技术选型是比较重要的一点,例如RocketMQ以及Kafka都是选择存储到本机,也就是本地文件系统,而Pulsar则是选择存储到分布式文件系统bookKeeper中,当然也有一些选择了分布式KV系统甚至是数据库,例如Redis自身也是支持publish/consume模型的,具体的选择哪一种实现方式只要还是看自己的业务场景,例如如果可靠性要求较高但对性能并不那么敏感的场景可以选择数据库作为存储介质。 选择本地文件系统去实现一个分布式消息队列相对来说是这几种最复杂的,不仅仅需要自己实现文件的IO细节,对于复制、一致性(当出现网络异常或者系统异常宕机时如何根据日志恢复系统的状态)也都需要自己实现,而这每一部分都需要相当一部分精力去研究,我们这次只是先首先一个比较简单的原型,对于这个方案之后有时间会搞。 基于分布式KV的方案相对来说也是不错的方案,性能很不错,而且接口也比较人性化,但是可靠性差了一点,对于类似交易、缓存同步这种对可靠性要求比较高的场景来说不那么适用。  基于数据库的方式性能上会有很大的损失,DB的数据结构本质上就不适合去实现消息队列,速度和一致性只能选择一个。这次我们选择利用分布式文件系统作为存储介质,例如HDFS、Apache BookKeeper等,我们分析一下Message queue的场景,单线程写-多线程读,这里需要引入topic分区的概念,一般如果某些topic比较活跃,吞吐量比较高,那么我们可以将消息分区,实现思路一般是将topic再从细粒度切分为子topic,并将每个子topic分布到不同的broker上,从而实现性能的线性提升,也就是说这里的单线程写具体指的是单个分区,多线程读相对来说比较容易理解,而HDFS正好适合这个场景,而且我们也不用去管replica、写分片、刷盘策略等等,减少了很多实现的复杂性,BookKeeper在这方面是不错的选择。  ## 客户端API实现 对于使用者而言,接触到的更多的是客户端暴漏的API,而客户端和服务器端Broker也需要一种方式通讯,对于RocketMQ以及Kafka都是选择实现了自定义的协议,消息队列的如果想要达到极高的吞吐量,实现一种高性能的网络通讯框架是相当重要的一环,RocketMQ是基于Netty之上构建的,而Kafka是直接基于NIO实现的,相对来说要复杂一点,如果看过源码的话会有所了解,Kafka客户端提交之后是先放到一个本地队列,然后根据broker、topic、分区信息等合并提交到服务器端,而Pulsar印象中是基于Protocol buffer实现的,这样相对自定义协议很多好处,首先如果协议后期实现过程有变动的话,如何兼容老的协议等这些细节已经由Protocol buffer帮你解决了,另外很重要的一点是,Protocol buffer可以帮你生成各个不同语言的API,如果是自定义协议这个又要费相当的精力去实现。 ## 一致性 对于消息队列的场景,每条消息都是一旦落盘之后,就不再支持更新操作,对于读取也都是顺序读,consumer抓取到的消息也都是已经落盘的或者已经commit的记录,因此一致性在消息队列中相对来说还是比较容易实现的。 ## 高可用...
> 本文简单记录一下如何搭建Apache Kafka 开发环境 # 前言 我这里用的是MBP,如果是Windows用户那么下面的命令以及一些路径可能会有差异,比如脚本后缀应该是.bat. # 安装相关库&&工具 1. JDK1.8 2. Gradle 3. [Zookeeper](https://zookeeper.apache.org/doc/trunk/zookeeperStarted.html) # 构建Kafka 1. https://github.com/apache/kafka, 下载源码 2. 执行以下命令 ```bash cd kafka gradle idea #这里会下载一坨依赖,慢慢等,看到Build Successful说明成功了 ```...