motan框架restful协议统一异常处理

通过自定义 EnpointFactory 处理 restful 协议的业务异常

motan 没有暴露 resteasy 提供的 @Provider 机制来处理异常,如果想利用这种机制,需要自己实现一个 EndpointFactory 来替换 motan 中自带的,插入自己实现的 @Provider 类

一个简单实现

  1. 新建一个 maven 工程,添加 motan restful 协议的 jar 包依赖到 pom 文件,注意 scope 为 provided

     <dependency>
         <groupId>com.weibo</groupId>
         <artifactId>motan-protocol-restful</artifactId>
         <version>0.3.1</version>
         <scope>provided</scope>
     </dependency>
    
  2. 新建一个用来统一处理异常的类,例如ExampleExceptionMapper,实现可以参照com.weibo.api.motan.protocol.restful.support.RpcExceptionMapper,注意接口ExceptionMapper<Exception>支持泛型
  3. 新建一个EndpointFactory实现类,例如ExampleEndpointFactory,继承com.weibo.api.motan.protocol.restful.support.AbstractEndpointFactory类,在类上添加@SpiMeta注解,指定 name 属性值,例如 ‘example’。
  4. 实现AbstractEndpointFactory类中的protected abstract RestServer innerCreateServer(URL url);方法,大部分代码可以照搬com.weibo.api.motan.protocol.restful.support.netty.NettyEndpointFactory 类中的实现,需要修改下面的一行代码,其余可以保持不变

         # 把 RpcExceptionMapper.class.getName() 替换为自己实现的统一异常处理类,例如上面实现的`ExampleExceptionMapper`类
         deployment.getProviderClasses().add(RpcExceptionMapper.class.getName());
    
  5. 在 META-INF 中新建 services 目录,在其中添加名为 com.weibo.api.motan.protocol.restful.EndpointFactory的文件 文件内容为自己实现的EndpointFactory实现类的全名
  6. 通过 maven 发布这个 jar 包,在需要统一异常处理的工程中依赖,修改其 motan.xml 文件的 <motan:protocol> 标签,指定endpointFactory的值为@SpiMeta中指定的 name 属性的值

     <motan:protocol id="restfulProtocol" name="restful" endpointFactory="example"/>
    

通过 filter 处理 exception

github issue 中 motan 开发人员给出的建议: 在service端,怎么处理 统一异常处理机制

原理类似,这里简单介绍实现方式

一个简单实现

  1. 新建 Java 类实现com.weibo.api.motan.filter.Filter接口中的Response filter(Caller<?> caller, Request request);方法,通过 @SpiMeta 注解在类上设置 name 属性
  2. META-INF/services目录中添加名为com.weibo.api.motan.filter.Filter的文件,内容为 Filter 接口实现类带有包路径的全名
  3. motan.xml中修改对应的<motan:service>标签,添加属性filter,值为第一步中自定义实现类@SpiMeta中指定的 name 属性值
  4. 业务逻辑抛出的原始异常可以通过下面方法获得

         Response response = caller.call(request);
         if (response.getException() != null) {
             Throwable cause = response.getException().getCause();
         }
    

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

参考:

  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"
     

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