响应式编程中生产者消费者速度不一致的应对方式

参考:

  1. Backpressure

在使用 rxjava 的过程中不可避免的会遇到上游的 Observable 生产数据的速度大于下游 Observer 消费的速度,这个时候需要选择一些策略来解决这个问题,按照 rxjava 官方 wiki 的说明,应对方式有三种,分别为丢弃或缓存、阻塞、back-pressure。

hot Observable 和 cold Observable

在了解这三种应对方式之前需要先了解 hot Observable 和 cold Observable,因为有些应对方式适用于不同类型的 Observable。

hot Observable: Observable 在创建完成时就可以发出数据,Observer 收到的数据序列是从订阅关系建立这一刻起的子序列,并且 Observable 按照自己的速度发出数据,Observer 无法控制发出数据的速度,必须自己解决消费速度问题,比如用户鼠标和键盘事件、系统事件或股票价格。

cold Observable: Observable 发出的数据序列是固定的,可以按照 Observer 的需要的时间和速度发出数据,比如数据库查询出的结果集、文件检索或网络请求。但是需要注意的是当一个 cold Observable 是 multicast (转换为一个 ConnectableObservable 并且 connect() 方法被调用过) 的,那么它应该被看作 hot Observable。

对于 cold Observable 的生产者速度过快问题,可以使用 back-pressure 策略,而 hot Observable 需要使用丢弃或者缓存。阻塞的情况比较特殊,下面再讲。

1. 丢弃或者缓存

通过 Operator 来缓存或者丢弃 Observable 发出的数据,使其速度小于消费者消费的速度,这样就可以不使用 backpressure 策略来让 Observable 降速。

丢弃:

使用 sample() 或者类似功能的 Operator throttleLast()、throttleFirst() 等,来过滤生产者发出的数据,把最终到达 Observer 的数据控制在一个合适的范围之内,被过滤掉的数据会被丢弃。比如某些不重要的日志,如果生产者速率过快,可以采取这种策略,丢掉部分日志。也可以作为某些不重要服务的降级策略。

缓存:

使用 buffer() 或者 window() Operator 缓存 Observable 发出的数据,稍后把这些数据批量发出,再由消费者决定怎么消费这些数据。

生产者发出的数据是均匀分布的:

假如生产者在 10 秒钟内均匀发出了需要保存到数据库的 10000 条用户数据,由于发出数据的速度过快,超过下游消费者保存到数据库的速度,那么可以用 buffer() Operator 将这 10000 条数据按照 100 毫秒为时间段收集为 100 个 集合,每个集合包含该时间段内的全部数据。然后再把这些集合发送给消费者,消费者可以选择把这些数据批量保存到数据库或者进行其他处理,从而达到降低速度的目的。

生产者发出的数据不是均匀分布的:

官方文档中介绍了 buffer() 配合 debounce() 来应对突发型数据的方法,假如生产者发出的数据不是均匀的,而是爆发式的,那么需要用 debounce() Operator 来监控 Observable ,在发现过了一个固定时间后 Observable 没有发出任何数据,就把当前 buffer() 内的数据作为一个集合发送给消费者。具体可以看官方的图,很直观。

2. 阻塞

通过调用栈阻塞的方式,阻塞 Observable,使其无法发出数据。这种方式有一个缺点是违背了『响应式』的初衷和非阻塞模型,但是假如被阻塞的线程不重要,可以被安全的阻塞(不影响其他部分),那么这也是一个可选的方案,不过现在的 rxjava 提供的 Operator 不会利用这种方式。

如果一个 Observable,所有操作它的 Operator ,所有订阅它的 Observer 都是在同一个线程上,那么实际上是通过调用栈阻塞形成了一种阻塞 back-pressure。不过要注意很多 Operator 默认情况下是在不同的线程,文档中有说明。

3. 背压(back-pressure)

消费者通过 “reactive pull” 来把生产数据速度过快的问题上移到生产者那里,让生产者去解决问题。

这个词在网络上有很多解释,这里只贴一下 ReactiveManifesto 术语表中的解释。

When one component is struggling to keep-up, the system as a whole needs to respond in a sensible way. It is unacceptable for the component under stress to fail catastrophically or to drop messages in an uncontrolled fashion. Since it can’t cope and it can’t fail it should communicate the fact that it is under stress to upstream components and so get them to reduce the load. This back-pressure is an important feedback mechanism that allows systems to gracefully respond to load rather than collapse under it. The back-pressure may cascade all the way up to the user, at which point responsiveness may degrade, but this mechanism will ensure that the system is resilient under load, and will provide information that may allow the system itself to apply other resources to help distribute the load, see Elasticity.

当系统中某些组件消费速度跟不上生产者生产的速度时,如果不想让组件崩溃或者以一种无法控制的方式丢弃消息,那么需要一种机制来把消费者组件的压力向上传递给上游的生产者组件,让生产者来减轻消费者的负荷。back-pressure 就是这样的一个让系统可以优雅的对负荷进行响应而不是被压垮的重要机制,把压力一直通知到用户那里,由系统维护者制定解决方案。

具体到 Operator 上就是利用 Subscriber.request(n) 的方式来向 Observable 请求数据,把获得数据从 push 模式转换为 pull 模式,从而控制生产者发送数据的速度。而 onBackpressureBuffer()、onBackpressureDrop() 等是为了应对没有实现 reactive pull 模式的 Observable 实现的辅助 Operator 。


进程从硬盘读取文件的过程

在知乎回答的一个问题,贴在这里 CPU与硬盘关系的几点疑问? - 中央处理器 (CPU) - 知乎

CPU 和硬盘的关系是不太好描述,CPU 本质上只是用来执行指令,具体的读取文件的操作是操作系统来做的,从操作系统的角度来说可能要方便一些。像其他答案说的,你的这些疑问应该去看操作系统和计算机组成原理相关的教材,形成一个整体上认识,而不应该片面的了解某一个方面。

我下面简单叙述一下操作系统在从硬盘读文件的流程。

为简单起见,假设场景是一个x86体系的32位Linux操作系统中运行的进程 P 需要读取文件 /home/user/test.txt,文件系统使用 ext3。

  1. Linux 系统提供了和 IO 相关的系统调用,进程 P 如果要读取文件,首先需要发起系统调用(System Call) open,传入文件路径”/home/user/test.txt” 和相关参数,来打开文件,执行系统调用以后操作系统会从用户态转换到内核态,部分 CPU 提供了”trap”或者”syscall” 指令来完成状态切换,切换到内核态以后,操作系统调用相应的处理器(handler) 开始处理读取文件的请求。
  2. 系统调用 open 并不会直接读取文件内容返回给进程,而是先进行权限方面的检查,如果进程可以访问这个文件,就根据文件路径去查找文件对应的 inode 编号。这部分属于文件系统的内容,在这里简单说一下,每一个非软链接的文件或者目录都具有一个惟一的 inode 编号,对应 inode table 中的一个 inode 数据结构,inode 中包含文件大小,修改时间之类的元信息,也包括一个树形的结构,这个树形结构里面索引了文件内容存储在硬盘的哪些扇区(sector)里。每个目录也有一个对应的磁盘文件来存储该目录中直接包含的子文件或子目录的名字和 inode 编号。比如查找”/home/user/test.txt”,需要按照路径逐级查找,首先根目录 / 的 inode 编号是约定的,为2,操作系统通过 inode 编号2这个信息去 inode table 中找到根目录对应的 inode 信息,根据 inode 信息读取磁盘扇区获取文件内容(这里已经需要访问磁盘了,下面再详细说访问过程),里面存储的内容比较复杂,为了提升检索效率可能会使用 B+树之类的进行了索引,这里为描述方便,简化一下,比如 根目录下有3个目录home、etc、bin,1个文件 eg.txt,那么根目录对应的文件内容大概类似于
     home 3
     etc 4
     bin 5
     eg.txt 5
     

    后面的数字就是目录或者文件对应的 inode 编号,通过检索可知,我们需要的找到 home 目录的inode 编号为3,重复这个过程,直到定位到 /home/user/test.txt 的 inode 编号。文件系统所有的 inode 信息也是存储在硬盘上的,也就是说硬盘中不仅有文件内容,还有文件系统的数据,这可以解释你的第二、三个问题,为什么换了硬盘仍然能够开机和识别文件,开机是存储在主板上的 BIOS 引导的,操作系统启动以后从硬盘读取文件系统的数据就可以获得整个磁盘的文件信息,CPU 只是执行操作系统的指令。

  3. 在获取 inode 以后,操作系统生成了一个文件描述符(file descriptor),存储在进程 P 自己的 file descriptors 数据结构中,通过文件描述符可以索引到文件的打开方式(只读、读写等)还有 要打开文件的 inode。然后操作系统将文件描述符返回给进程 P,至此系统调用 open 完成。
  4. 进程 P 获取到文件 /home/user/test.txt 的描述符以后,还需要再发起系统调用 read,传入文件描述符来读取文件内容,同样读取操作需要切换到内核态由内核代为完成。切换到内核态以后,操作系统通过文件描述符找到对应的 inode ,通过 inode 来确定文件存储在磁盘哪些扇区中,然后向磁盘发送指令来读取这些扇区,把内容读取到内核的地址空间里面。一般来说操作系统会通过 memory mapped IO 技术把键盘、磁盘等硬件上的寄存器连接到 IO 总线,再通过 IO 控制器连接到内存总线,这样硬件上的寄存器也被映射到了一段内存地址上,CPU 可以直接通过读写内存的指令来读写硬件寄存器中的数据。同时还会通过 DMA 技术来让硬件不通过 CPU,直接读写内存的内容,这样磁盘在传输文件的同时 CPU 可以去执行其他线程。
  5. 磁盘的 IO 操作完成后,磁盘会触发一个中断(interrupt),CPU 会暂时中止当前线程的执行,保存相关的寄存器信息后,调用对应的中断处理器(interrupt handler),把读取到的内容从内核地址空间拷贝到进程 P 的地址空间里面,然后将进程 P的状态设置为 runnable, 进程 P 排队等待自己的 CPU 时间片,被调度器调度以后可以继续执行。

支持中文搜索的gitbook

如何安装

fork项目地址: codepiano/gitbook · GitHub

官方仓库地址: GitbookIO/gitbook · GitHub

由于使用的分词插件模块nodejieba,是使用nodejs对c++库的封装,所以安装依赖的时候要求机器上安装有c++编译环境

windows的情况我不了解,linux使用的是g++,mac使用clang ,可能需要安装一下 command line tools ,具体安装方式请google

npm install -g codepiano/gitbook

添加的插件

  1. 集成多说评论,见 codepiano/gitbook-plugin-duoshuo
  2. 集成畅言评论,见 codepiano/gitbook-plugin-changyan

    进行的主要改动

  3. 支持中文搜索 需要生成所有文章内容的json格式索引文件,内容过多的话需要注意文件的体积,开启压缩的话应该不会造成太大的负担
  4. 支持搜索后关键字高亮
  5. 支持分享到qq、微信(二维码)
    • 分享到qq直接跳转至qq connect分享页面,该页面也可实现分享到qq空间和qq微博的功能
    • 分享到微信直接生成二维码,也可分享到其他平台
    • 需要在配置文件中设置相关选项为true
  6. 修改了介绍页面(即生成网站的主页)的方式,增加了一个新的指定方式
    • 原始的gitbook介绍页面文件名必须为README.md,只可以自定义标题名称
    • 修改后可以在配置文件book.json中指定介绍页面的标题和文件(相对)路径,可以不在SUMMARY.md文件中指定,在book.json中添加

        "introduction": {
            "path": "你的介绍文件的路径",
            "title": "你的介绍文件的标题"
        }
        
  7. 可以在目录栏底部添加自定义链接,在book.json中添加配置项

     "tail": {
         "tilte1": "url1",
         "title2": "url2"
     }
     
  8. 不再把google、facebook、twitter分享设置为默认,所有分享需要在配置文件中设置,才会出现在分享栏。

     "links": {
         "sharing": {
             "all"      : true,
             "google"   : true,
             "facebook" : true,
             "twitter"  : true,
             "weibo"    : true,
             "qq"       : true,
             "qrcode"   : true
         }
     }
     
  9. 为了便于SEO,添加了keywords的meta标签,可以在book.json中配置

     "keywords": "keyword1,keyword2,keyword3"
     

溯洄于时光的洪流

总是在不知不觉中,迎来又一个新年,仿佛清晨被人从酣睡中唤醒:“喂,再不起床就迟到了”,你的大脑瞬间紧绷,一切事物都被慌张和焦急浸湿、染色,这个世界被重新拧紧了发条,秒针的每一声嘀嗒都清晰可辨。时间和生活逐渐远去,就像追赶不上的公车,最后你终于发现并且承认,这一年又被消磨掉了。

不知道从什么时候,开始感觉时间不够用,于是想办法利用零碎时间来看书,成为习惯后才意识到,时间真的像海绵里面的水,只要你愿意,可以挤出来很多,以前还无知的认为这种名人名言除了作为考试题没有任何价值。如《道德经》所说:“上士闻道,勤而行之;中士闻道,若存若亡;下士闻道,大笑之。不笑,不足以为道”,无知且自以为能确实非常愚蠢。两年前给自己定了目标,争取每周看一本书,通过对零碎时间的利用,一直坚持了下来,由于上半年时间比较充裕,还超额不少,这或许是我长这么大惟一值得骄傲的事情,如果有精力,会一直坚持下去。

庆幸在很早的时候就接触了新浪微博和知乎,有机会了解到很多聪明的人,从他们的言论里面获得了很多经验和知识,经常有听君一席话,胜读十年书的感觉。同时将很多“不知道自己不知道”转变成“知道自己不知道”,借用知乎的一句宣传词,就是“发现更大的世界”,免得成为坐井观天的夜郎。

经过一年的尝试,英文阅读水平进步很大,看了一二十本英文书了,阅读速度达到了可以接受的程度,尽量做到了有原版书的情况下不读译本。而且互联网上的很多知识,也是别人读了英文的资料,再加工出来的,这个过程失真和噪声很大。现在回想起来,当初决心看英文书籍的原因也很简单,一是发现别人的知识来源基本都是英文,帮人解答问题都是直接给英文资料链接,二是关注的刘未鹏 | Mind Hacks | 思维改变生活推荐的书籍基本都是英文的。现在回想起来,对零碎时间的利用也受了他那篇《暗时间》的影响。还记得看的第一本英文书是《Hackers and Painters》,当时查字典断断续续看了三四个月才看完,习惯以后看书就轻松了些,受这本书的影响还学了学lisp语言,感觉就像发现了另一个宇宙。

今年年初的时候列个了时间表,规划了下要学的内容,现在回过头来看,只能说可以,但是还不够好。今年依旧列了个计划,希望能有所突破。最近看到一条微博,说三十岁以后想再有时间去学习知识简直是奢望。虽然离三十岁还有五六年,不过已经感觉到确实时间不那么充裕了,很多事情都要分心去了解和处理,人总不能在书堆里过日子。趁现在有时间,能学多少是多少吧。

总是听到这样的言论,“要是高中好好学就好了”、“要是大学好好学就好了”、“要是多看看书就好了”等等。我以前也这么后悔过,不过有一天意识到:大学的时候后悔高中没好好学,工作以后后悔大学没好好学,工作遇到瓶颈后悔刚工作的几年没多看看书,如果你只是这么想而不去想办法努力并改变的话,三年以后,你会后悔现在没好好学,六年以后,你会后悔三年前没好好学,很多人的一生,就是这么蹉跎的吧。

很喜欢写代码和学习计算机科学相关的内容,希望新年能有突破。换了新机器后把机器名改成了lunar mare,也就是月海,这个词是早期的天文爱好者观察月球时,受限于器材和知识,认为月球表面有美丽的海洋,但实际上那些只是类似于盆地的地貌。计算机科学也是这样类似的东西,远远望着觉得很神奇很精彩,接触之后会发现看起来十分乏味和贫瘠,但是当你深入研究之后,才能真正的热爱上这个学科。

突然想起一件小事,随手记在这里,北航六十年校庆的时候去玩,在学校里面的航空博物馆看神舟七号发射的动画演示,运载火箭从上到下包括逃逸塔,整流罩,多节火箭,助推器等等,发射过程要逐步的脱落。旁边两个学生,看样子像情侣,他们的对话很有意思,顿时让我想起“男人来自金星,女人来自火星”这句话。

        火箭助推器脱落
        女(呆萌脸):呀,脚没了
        男(正经脸):嗯,火箭的四个助推器脱落了
        火箭逃逸塔脱落
        女(呆萌脸):呀,头没了
        男(正经脸):嗯,火箭要抛掉逃逸塔,这个是用来救生的
        一级火箭脱落
        女(呆萌脸):呀,腿也没了
        男(正经脸):嗯,一级火箭脱落,二级火箭要开机了
        火箭整流罩脱落
        女(呆萌脸):哎呀,脖子也掉了
        男(正经脸):嗯,火箭要抛掉整流罩,这个会被回收回来,里面的黑匣子记录有数据
        星箭分离
        女(呆萌脸):呀,肚子也没了
        男(正经脸):嗯,二级火箭关机,要星箭分离
        
        然后女生蹦蹦跳跳走了,男生在后面老老实实跟着
    

—  原创作品许可 — 署名-非商业性使用-禁止演绎 3.0 未本地化版本 — CC BY-NC-ND 3.0   —