yum update
更新时,遭遇报错:Error:Failed to download metadata for repo 'appstream': Cannot prepareinternal mirrorlist: No URLs in mirrorlist 的解决方法。在CentOS8中,执行yum update
更新时,遭遇报错:
1 | Error: Failed to download metadata for repo 'appstream': Cannot prepare internal mirrorlist: No URLs in mirrorlist |
根据提示,显示mirror list中找不到可用的URL,导致无法获取appstreamrepo的元信息。
我查到的资料中,已有的解决方案是通过sed工具批量查询和替换/etc/yum.repos.d/
中的软件仓库配置信息。
引用自:https://www.cnblogs.com/EthanWong/p/15932675.html
1
2
3
4
5
6 # 进入yum.repos.d 目录下
cd /etc/yum.repos.d/
# 修改源链接
sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-*
# 要将之前的mirror.centos.org 改成 vault.centos.org
sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-*
上述文章指出,因为CentOs Linux 8 从 2021.10.31号后已经停止维护,所以之后更新镜像需要通过vault.centos.org
来获取更新。相应的,文中给出的方案就是将/etc/yum.repos.d/
中,各个软件仓库配置文件中的mirrorlist
字段都注释掉,不采用镜像列表,而是启用baseurl
去连接源站,且原站修改为http://vault.centos.org
。
我认为上述修改软件配置的方案也许可行,但问题的实质是因为:RHEL修改了CentOS的开源方案,将CentOS改为CentOSStream的形式进行后续迭代。过去的CentOS与RHEL共享核心代码,只是RHEL额外具备一些增值软件和服务。目前的CentOSStream将作为RHEL的“开发版”,即:开发的新代码先发布到CentOSStream上进行验证,再合入RHEL。通过Fedora Linux, CentOSStream这依次两道试验阶段,再合入RHEL,以保障RHEL的高可靠。
CentOS Stream
Continuously delivered distro that tracks just ahead of Red HatEnterprise Linux (RHEL) development, positioned as a midstream betweenFedora Linux and RHEL. For anyone interested in participating andcollaborating in the RHEL ecosystem, CentOS Stream is your reliableplatform for innovation.
CentOS Linux 8因为这项变更,EOL被改为2021年底,因此2022年开始,CentOSLinux 8的mirrorlist下架,代码停止维护。
因此,该问题解决方法的最佳实践应当是根据官方文档,将已经EOL的CentOSLinux 8迁移到CentOS Stream8的发行分支上。(或采用其他Linux,如:Debian)。
https://www.centos.org/centos-stream/
1
2 dnf --disablerepo '*' --enablerepo extras swap centos-linux-repos centos-stream-repos
dnf distro-sync
dnf是CentOS8默认的包管理器。按照上述命令,通过dnf重新配置软件repo并同步数据,即可迁移到CentOSStream发行分支上。
DNF stands for Dandified YUM is a software packagemanager for RPM-based Linux distributions. It is used to install, updateand remove packages in the CentOS operating system. It is the defaultpackage manager of CentOS8.
迁移后的CentOS Stream 8的EOL为May 31st, 2024。
]]>Slice的实现位于go.go,总共仅318行。
本文以目前Go源码最新的1.17.2版本为例。
1 | type slice struct { |
slice的数据结构并不复杂,本质上是对array的一层封装,类似Java中的ArrayList。
slice底层数据由array存储,由len标记当前实际存储的元素数量,cap标记当前array指针指向的内存对象的元素容量。
1 | func makeslice(et *_type, len, cap int) unsafe.Pointer { |
构造过程输入et,即ElementType的缩写,用于记录slice中存储的元素类型、
首先,通过math.MulUintptr
函数实现带溢出检测的uintptr类型乘法。
https://pkg.go.dev/runtime/internal/math#MulUintptr
https://cs.opensource.google/go/go/+/go1.17.2:src/runtime/internal/math/math.go;l=13
math.MulUintptr函数的实现挺巧妙的,此处暂不深究
随后,根据计算出的内存长度,通过mallocgc函数(位于go.go中,基于TCMalloc机制实现)分配相应的内存对象。
slice能够在append时自动扩容。
1 | // growslice handles slice growth during append. |
在扩容时,如果新容量已经超过现有容量的两倍,则以更大的新容量为准。
如果指定的新容量不足两倍,则分两种情况:
runtime:make slice growth formula a bit smoother 不过值得注意的是,这样的扩容算法未必是最优的,仍然存在改进的研究空间。从master分支上最新commit中可以看到,新的commit正在尝试更平滑的扩容函数(及参数)。高的增长倍率,一方面有助于避免频繁扩容(避免分配内存时潜在的系统调用代价),另一方面也更容易造成内存冗余。
此后,计算新slice的array所需的内存容量capmen和相应的元素容量newcap。(该计算过程针对元素尺寸做了优化)
最后,通过mallocgc函数申请capmem尺寸的内存对象,并且用memmove函数将原slice数据拷贝到新slice的内存(指针p)中。
]]>Gin is a web framework written in Go (Golang). It features amartini-like API with performance that is up to 40 times faster thanksto httprouter.If you need performance and good productivity, you will love Gin.
LearnGin
LearnGin仓库存储本文的示例代码。
本文所使用的软件版本是:
主函数位于项目根目录下的main.go中,代码如下:
1 | package main |
主要步骤:
gin.Default()
执行Gin的初始化过程,默认的初始化包含两个中间件,middleware.RegisterMiddleware(r)
用于将项目中开发的中间件注册到GinEngine上;handler.RegisterHandler(r)
用于将项目中开发的对应于指定URL的事件处理函数注册到GinEngine上;r.Run()
负责启动GinEngine,开始监听请求并提供HTTP服务。1 | // Default returns an Engine instance with the Logger and Recovery middleware already attached. |
Gin的默认初始化主要是创建Engine和注册默认的两款中间件。
1 | package middleware |
gin.Engine的r.Use
函数负责将gin.HandleFunc类型函数注册为中间件。此处的debug.DebugMiddleWare()
是本例开发的一个简易的自定义中间件,用于在实际的事件处理前,输出详细的请求信息;在实际的事件处理后,输出结果状态码。
Engine.Use函数用于将中间件添加到当前的路由上,位于gin.go中,代码如下:
1 | // Use attaches a global middleware to the router. ie. the middleware attached though Use() will be |
实际上,还需要进一步调用engine.RouterGroup.Use(middleware...)
完成实际的中间件注册工作,该函数位于gin.go中,代码如下:
1 | // Use adds middleware to the group, see example code in GitHub. |
该函数也很简短,实际上就是把中间件(本质是一个函数)添加到HandlersChain类型(实质上为数组type HandlersChain []HandlerFunc
)的group.Handlers中。换句话说,实际上是以函数数组的形式收集了一个有序的函数序列。
此后会介绍中间件中每次都会出现的c.Next()
函数如何基于该数组进行流程控制。
1 | package handler |
gin.Engine的r.Handle
函数用于将事件处理函数注册到指定的HTTP方法+相对路径上。
1 | // Handle registers a new request handle and middleware with the given path and method. |
GinEngine的Handle函数调用实际上调用的是内部匿名属性RouterGroup的Handle函数。该函数的逻辑由handle函数进一步处理,代码为:
1 | func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes { |
可以看到,实际上handler是由group.engine.addRoute(httpMethod, absolutePath, handlers)
注册路由的。
如果持续追查下去,会发现addRoute函数实际上是将该方法添加到当前HTTP方法对应的那颗路由树中。
1 |
|
每个HTTP方法(如:GET,POST)的路由信息都各自由一个树结构来维护,该树结构的模型与函数实现位于gin/tree.go中,此处不再继续展开。
1 | // Run attaches the router to a http.Server and starts listening and serving HTTP requests. |
可以看到,Engine.Run函数主要是:
其中,最核心的监听与服务实质上是调用Go语言内置库net/http的http.ListenAndServe
函数实现的。
Gin框架网络编程的底层实际上是基于Go语言的内置net/http网络库实现的。
1 | // ListenAndServe listens on the TCP network address addr and then calls |
该函数实例化Sever,并调用其ListenAndServe
函数实现监听与服务功能。
注意:此时,输入的GinEngine对象以Handler接口的对象的形式被传入给了net/http库的Server对象,作为后续Serve对象处理网络请求时调用的函数。
net/http的Server结构体类型中有一个Handler接口类型的Handler。
1 | // A Server defines parameters for running an HTTP server. |
而该Handler接口的唯一特点就是有且仅有一个ServeHTTP函数声明,该接口定义代码如下:
1 | // A Handler responds to an HTTP request. |
Handler接口的意义就在于,任何类型,只需要实现了该ServeHTTP函数,就实现了Handler接口,就可以用作Server的Handler,供HTTP处理时调用。
显然,gin.Engine实现了net/http的Handler接口的ServeHTTP函数(gin/gin.go)。具体的实现原理在接下来介绍。
上文介绍到,gin实际上调用了net/http的ListenAndServe
函数实现网络监听与处理,具体由Server.ListenAndServe
实现,位于net/http/server.go中,代码如下:
1 | // ListenAndServe listens on the TCP network address srv.Addr and then |
可以看到,net/http的Server.ListenAndServe
函数实际上主要完成两项工作:
net.Listen("tcp", addr)
负责设置监听地址;srv.Serve(ln)
负责在监听位置上接受网络请求,建立连接并做出响应。Server.Serve
函数用于监听、接受和处理网络请求,代码如下:
1 | // Serve accepts incoming connections on the Listener l, creating a |
在Server.Serve
函数的实现中,启动了一个无条件的for循环以便持续监听、接受和处理网络请求,主要流程为:
l.Accept()
调用在无请求时保持阻塞,直到接收到请求时,接受请求并返回建立的连接;go c.serve(connCtx)
);已接受的请求会建立连接,对连接的后续处理由conn.serve函数实现,该函数实现较长,代码如下:
1 | // Serve a new connection. |
不难发现,conn.serve
函数的代码实现较长,其中,对连接的主要处理由serverHandler{c.server}.ServeHTTP(w, w.req)
函数调用实现。
这一步调用实质上时首先实例化了一个Server实例,然后调用实例的ServeHTTP
函数对连接的请求与响应进行具体的处理。上文讲到,实现了ServeHTTP
函数就实现了Handler接口。Gin就是通过实现接口的方式,利用系统的net/http库执行自身的功能。
gin在gin.go中实现了ServeHTTP
函数,代码如下:
1 | // ServeHTTP conforms to the http.Handler interface. |
主要步骤为:
http.ResponseWriter
实例与http.Request
实例;engine.handleHTTPRequest(c)
封装实现了;值得注意的是,Gin中对每个连接都需要的上下文对象进行缓存化存取,通过缓存池节省连接高并发时上下文对象频繁生灭造成内存频繁分配与释放的代价。
handleHTTPRequest
函数封装了对请求进行处理的具体过程,位于gin/gin.go中,代码如下:
1 | func (engine *Engine) handleHTTPRequest(c *Context) { |
Engine.handleHTTPRequest
函数的主要处理位于中间的for循环中,主要为:
engine.trees
以找出当前请求的HTTPMethod对应的处理树;value
;gin.HandlerChain
)写入当前连接上下文的c.handlers
中;c.Next()
,调用handlers链上的下一个函数(中间件/业务处理函数),开始形成LIFO的函数调用栈;c.writermem.WriteHeaderNow()
根据上下文信息,将HTTP状态码写入响应头。请求发来时,被中间件与业务逻辑的handler处理,Gin的中间件与业务逻辑函数实质上都是gin.HandlerFunc函数。
例如,为gin.Engine添加了两款中间件(MiddeWareA与MiddleWareB)并为GET方法的/hello路径注册了一个Hello函数作为路由处理函数,那么执行过程为:
handleHTTPRequest
函数执行到c.Next()
,调用MiddleWareA;c.Next()
,调用MiddleWareB;c.Next()
,调用Hello;中间件中屡屡调用的c.Next()
函数时gin提供的中间件流程控制函数之一,位于gin/context.go中,代码如下:
1 | /************************************/ |
不难理解,Next
函数起到的作用是,在当前中间件函数中,调用下一个HandlerFunc。依序调用HandlerChain中的HandlerFunc的过程中,形成了一个函数调用栈,调用时函数依序入栈,至最后一个函数调用返回,此后按LIFO的顺序出栈,自然就形成了上述中间件的LIFO的执行顺序。
在本例中,我写了一个简易的创建Person的API,其涉及到模型定义与业务逻辑。
模型定义位于/model/person.go中,代码如下:
1 | package model |
其中,BaseResp位于/model/base.go中,代码如下:
1 | package model |
业务逻辑函数位于handler/person/create_person.go中,代码如下:
1 | package person |
处理上主要分三步:
c.ShouldBindWith(req, binding.JSON)
负责解析请求中发来的JSON数据,并将解析结果绑定到指定的结构体对象上;值得注意的是:此处的序列化与反序列化会参照结构体的类型tag(如有)。
结合对Gin框架主干代码以及其调用的部分Go源码的阅读,可以体会到:
本文参考自:
TCMalloc: Thread-Caching Malloc Sanjay Ghemawat, Paul Menage
opensource@google.com
TCMalloc(Thread-CachingMalloc)是Google发布的一款线程缓存型内存分配机制。TCMalloc为每一个线程都缓存一些可分配内存,因此,在多线程场景下,TCMalloc能够尽可能规避多个线程同时分配/释放内存时的锁争用问题,这使得TCMalloc相较于其它内存分配机制,内存分配和回收速度更快。另外,TCMalloc还有内存分配利用率高的优势。
TCMalloc通过Thread Cache和Central Heap组成的双层结构分配内存。
线程分配内存时,TCMalloc从该线程的线程缓存(ThreadCache)中取出恰当尺寸的内存块。而线程释放回线程缓存的内存,也会由垃圾回收机制收纳回中央堆区(CentralHeap)。具体地,TCMalloc的内存分配分两种情况:
当线程请求分配不超过32KB的小对象时,线程缓存为其分配恰当尺寸的内存块。
线程缓存(ThreadCache)维护着一个数组到单向链表的数据结构,数组中的每一个节点都从小到大依次代表一个可分配尺寸(共约170种尺寸),每个尺寸以单链表的形式维护该尺寸的可分配内存。
当一个线程请求分配内存时:
超过32K的大对象以4K的内存页为单位进行分配。直接由中央堆区负责维护空页,通过链表分别归纳维护长度为1~255页的空内存块,长度超过255页的内存块则由rest链表统一管理。
当请求内存时,根据需求的页面数量找到对应页面数的链表;
TCMalloc通过span对象来组织内存页。一个span代表一些连续的内存页。
通过一个数据结构来维护从页号到span地址的映射:
在32位环境下,32位的地址能够寻址232B的内存空间,如果按每个内存页4KB的尺寸进行分页,总共220,即1M个页号。每个span地址为32位,即4B,那么通过4MB的数组就能够实现从页号到span地址的寻址。
在64位环境下,考虑到地址空间很大,因此通过一个3层的基数树(radixtree)来建立页号到span地址的映射。
当内存对象释放时,首先根据内存页号查出对应的span对象。通过span对象进行判断:
当线程缓存中的空余内存超过阈值(默认2MB)时,会触发垃圾回收,把线程缓存中的内存还给中央堆区的空闲链表。
当线程数量增加时,垃圾回收阈值会减小,以免线程数量很多时浪费内存空间。
垃圾回收机制会记录线程缓存中每一个空闲链表的低水位L。L指的是自上一次垃圾回收以来该链表的最短长度。TCMalloc每次将空闲链表中L/2个内存对象回收到中央堆区。每次回收L/2,这样的回收速度能够很快地将长期不用的空闲链表回收到中央堆区的空闲链表中,以便其他有需要的线程快速获取。
]]>GoWiki是一个极简的GoWeb应用,使用Go语言内置的html/template
和net/http
等库实现,实现基本的百科网站功能,包含词条创建、编辑、保存和浏览功能。
本节内容总结自官方教程
gowiki
wiki.go
edit.html
view.html
1 | // Writing Web Applications |
1 | <h1>Editing {{.Title}}</h1> |
1 | <h1>{{.Title}}</h1> |
单文件go程序,通过以下命令即可运行:
1 | go run wiki.go |
或编译后再运行:
1 | go build wiki.go |
从上节可以看到,Go语言的net/http
和html/template
已经足够实现基本的Web应用,但Go自带的路由http.ServerMux
机制简单,只能实现从请求路径(string)到处理函数(handler)的映射,无法根据HTTP的方法(Method),请求头(header)进行路由。GoWeb框架实现了比内置库更丰富的功能,例如
Gin is a web framework written in Go (Golang). It features amartini-like API with performance that is up to 40 times faster thanksto httprouter.If you need performance and good productivity, you will love Gin.
此外,还有其他Go Web框架,如:
Go语言没有内置数据库驱动。
Go语言定义了database/sql
接口,分离出接口实现与接口调用,使得调用方改换数据库时无需修改代码。
参阅:
longjoy/micro-go-book/ch5-web/mysql/mysql.go
1 | func init() { |
Go语言的结构体和NoSQL的JSON可以很好地直接对应起来,因此,Go语言中一般可以直接操作NoSQL,不依赖ORM。
参阅:
longjoy/micro-go-book/ch5-web/mongo/mongo.go
1 | func connect(cName string) (*mgo.Session, *mgo.Collection) { |
Beego is used for rapid development of enterprise application in Go,including RESTful APIs, web apps and backend services.
It is inspired by Tornado, Sinatra and Flask. beego has someGo-specific features such as interfaces and struct embedding.
Beego是一个简单易用的企业级Go应用开发框架,其中包含了ORM框架。
Beego的ORM的具体使用方法可以参阅其文档:
]]>
1 | import matplotlib.pyplot as plt |
主要的绘制要点在于:
cmap
)。对投资做的好的大师,首先必须是控制风险的大师。
为了(可能不确定的)将来的消费(价值)而牺牲现在一定的消费(价值)。
家庭收入
企业收入用于
企业的工资福利流入家庭,家庭的投资和消费流入企业。
资金来源
政府收入用于
企业部门的金融负债和居民部门的金融资产接近。
宏观经济上,企业部门的负债主要来源于居民部门的金融资产。
当股票等金融资产价格呈现上涨趋势时,
形成金融泡沫。
虚拟经济与实体经济是此消彼长,又相辅相成的。
Q = 公司的市场价值/公司的重置成本
实际情况Q一般大于1,因为存在专利、壁垒等……
C+S=C+I
居民消费C+居民储蓄S=企业所生产产品C+企业投资I
加入政府和国际经济体时:
C+S+T+Kr=C+I
维持经济稳定,应对金融危机的4万亿投资计划
影响:
负面效应:
只有生产和消费。
储蓄总是等于投资。
生产三大要素:资本K、劳动L、知识A。
资本投入存在边际效应。
索洛模型表明:在具有相同生产函数、储蓄和折旧率情况下,经济体最终所达到的均衡状态与初始状态无关。
提高储蓄率,可以增加资本投入,继而提高人均产出。
但是,储蓄率过分提高,会牺牲一代人的消费。
将技术进步纳入内生变量。
纳入技术进步后,资本的边际报酬不再递减。
案例:供给侧改革
由于投资存在乘数效应且产出存在加速效应,因此到达一定峰值后,经济就会衰退,到达低谷后,一些投资需求被刺激,又进入上升通道。
认为投资和消费本身的特性产生了经济周期。
认为经济周期是外部因素引起的。
熊彼特的创新周期论。
卡莱斯基的政治周期论。
年度投资规模(短期、流量)
在建投资规模(长期、存量)
投资主体结构:
Capital-Output Ratio, COR
Incremental Capital-Output Ratio, ICOR
当市场有效且达到均衡时,各个部门的资本边际收益率呈现均一化的特征。
(否则资本会从低收益部门流向高收益部门)
调整推算法:对统计数据要求很高;
函数估计法:需要假定生产函数。
一个企业群,群内各成员所生产的商品对消费者时可互相替代的。
道琼斯分类法
联合国国际标准行业分类法
我国国民经济行业分类法
我国上市公司行业分类法
四个阶段:
特征 | 创业阶段 | 成长阶段 | 成熟阶段 | 衰退阶段 |
---|---|---|---|---|
行业规模 | 较小 | 扩大 | 饱和 | 缩小 |
产出增长 | 较快 | 很快 | 较慢 | 很慢,甚至为负 |
利润水平 | 低 | 高 | 低 | 亏损 |
技术创新 | 较快 | 逐渐稳定 | 稳定 | 淘汰或被替代 |
竞争者数量 | 很多 | 增多 | 下降 | 降至不足 |
开工率 | 提高 | 满负荷 | 下降 | 降至不足 |
资本进退 | 进大于出 | 进大于出 | 进出平衡 | 进小于出 |
其他高壁垒行业、政府介入行业等,难以用行业生命周期解释。
增长型 | 周期型 | 防御型 | |
---|---|---|---|
与经济周期的关系 | 受经济周期影响不大 | 与经济周期直接相关 | 产品需求相对稳定,受经济周期影响较小 |
增长的核心来源 | 技术进步等不受经济周期影响的因素 | 居民收入等受经济周期直接影响的因素 | 居民刚性需求 |
案例 | 计算机相关行业 | 汽车等行业 | 医药、生活必需品等行业 |
经济繁荣时 | 增长 | 增长 | 相对稳定 |
经济衰退时 | 增长 | 衰落 | 相对稳定 |
行业轮动是根据商业周期状态预测业绩卓越的行业或部门,并将投资组合转向这些行业或部门。
宏观经济周期分为四个阶段:
投资策略:
适用情况:
特征 | 完全竞争 | 垄断竞争 | 寡头垄断 | 完全垄断 |
---|---|---|---|---|
企业数目 | 众多 | 很多 | 较少 | 单个企业 |
生产要素流动性 | 完全自由流动 | 自由流动 | 较难流动 | 不流动 |
产品差异性 | 同质无差别 | 存在差别 | 同质或存在差别 | 无 |
企业定价能力 | 企业仅接受价格,无法制定价格 | 企业对价格有控制能力 | 企业对价格具有垄断能力 | 企业垄断定价,但受到法律管制 |
典型行业 | 初级产品(例如:农产品) | 家电、洗发水等消费品 | 资本、技术密集型行业,少数储量集中的矿产品 | 公共事业,资本、技术高度密集型行业,稀有金属矿藏开采行业 |
集中度体现在行业前K名的累计市场份额。
集中度曲线:
散点市场->块状同质化市场->团状异质化市场
列表格,对要素打分。
行业市盈率=行业(价格)指数 / 行业利润率
行业指数代表投资者对行业的估值;
行业利润率代表行业的盈利能力。
投资策略:同等条件下,尽量选择行业指数和行业利润率较高,而市盈率较低的行业。
注意:行业之间的市盈率不具备可比性。
利用行业的历史数据回归估计行业收益率。
注意:有效市场中,未来股价不受过去股价影响,用现在收益率难以预测未来收益率。因此一般很少用定量分析预测行业未来,常用定性分析和经验。
略
金融工具一般按期限来分类:
债券市场可追溯到1792年纽约股票交易所。
形式:
风险:安全性非常好,近乎于现金等价物。
期限:
目的:
发行:
地方政府募资发行。
目的:
规模:远远大于国债发行数量。
还本付息受财政、地方发展水平影响,信用比国债低些。
中央银行票据,调节商业银行超额准备金而发行的短期债务凭证,实质时中央银行债券。
目的:不是为了筹集资金,而是央行调节基础货币的货币政策工具,为了减少商业银行可贷资金量。
因资金流向债券,商业银行需要吸引储蓄稳定存款。
存单注明存款期限和利率,可到期取本息,也可到期前转让,可在二级市场流通。
银行和基金公司间流通,同业拆借。
回购协议:先卖出债券,再回购。相当于以债券作为抵押品。
金融机构发行的债券。
企业发行的债券,受证监会管理。
风险和收益都高于国债。
在西方,企业债即公司债。
在我国,企业债券是中央政府部门所属机构、国有独资企业或国有控股企业发行的债券。
企业债的发行与政府部门的审批项目直接相关,发行由发改委审批。
金融公司或高信用企业开出的无担保短期票据。
期限在1个月~1年,通常滚动发行,为旧票据还本付息。
企业在银行间市场发行,由金融机构购买,不向社会发行,一年期内还本付息的有价证券,是短期贷款的替代品。
无担保、短期、需评级。
企业在银行间市场发行,是中期贷款的替代品。
可选是否将债券一定比例转为股权,赋予了债券一定程度的期权能力。
可以在海外发行的债券。
中小型企业的募资需求。
资产证券化(AssetSecuritization)是指以特定资产组合或特定现金流为支持,发行可交易债券的一种融资形式。
起源于1970年美国发行的以抵押贷款组合为基础资产(如:住房抵押贷款)的抵押支持债券(MBS,Mortgage-BackedSecurity),此后从抵押贷款发展到其他资产上(如:汽车贷款、消费贷款等),出现资产支持债券(ABS,Asset-Backed Security)。
本质特征:
影响:
承担债务,需还本付息。例如,抵押贷款中的借款方。
享有债权。例如:抵押贷款中的放款银行。
作为资产证券化的发起人,原始债权人把需要证券化的资产出售给特别目的机构,实现资产风险与收益的充足。
从发起人处购买可证券化资产,并发行以此为支持的证券的特殊实体。
一般是不会破产的高信用等级实体。
因为原始债权人将资产真实销售(truesale)给SPV,所以证券化资产的风险与原始债权人的风险可以隔离开来,实现破产隔离(bankruptremote),即使原始债权人破产,也不会影响到投资人对证券化资产的权益,提高了证券化资产的资信评级,降低了融资成本。
购买证券的机构或个人。
因为这类证券通常高收益低风险,因此一般是机构投资者购买,如保险公司、投资基金和银行机构。
一般由发起人兼任。
负责按期收取证券化资产所产生的现金流,并转移给SPV或SPV指定的信托实体。
由SPV指定的负责对专门服务人收取的现金流进行管理,并向证券投资者按时支付的机构。
通过对资产证券化各个环节进行评估而给出信用等级的机构。
对证券进行信用增级,降低发行成本。
为SPV发行证券提供担保的机构,为证券进行信用增级。可以是政府担保机构或私人担保公司。
为SPV所发行证券进行承销的实体,确保证券销售成功。一般是投资银行,或组建的承销团。
优先股(preferredstocks)具有权益和债务的双重特征,是再剩余索取权方面较普通股优先的股票。
股息:通常归结为固定收益工具,它与债券一样,都承诺支付定量的股息(事先固定)。
优先性:再分得公司利润时和破产清偿时顺序优于普通股,但都低于债权。
股东权利:优先股在剩余控制方面劣于普通股,不能参与公司的经营管理,没有选举董事会和监事会的权利。
普通股(commonstocks)是在优先股要求权得到满足之后才参与公司利润和资产分配的股票合同,股息收益上不封顶、下不保底,每一阶段的红利数额也不确定。
股东权利:有出席股东大会的会议权、表决权和选举权、被选举权等,通过投票(通常一股一票和简单多数)来行使剩余控制权。
案例:阿里巴巴的双层结构的普通股,分为:
人民币普通股
人民币特种股票。以人民币标明面值,以港币或美元交易。
国企股,注册地在内地,上市地在香港的股票。
注册地在中国大陆,上市地在纽约证券交易所。
注册地在中国大陆,上市地在伦敦。
稳定盈利的大公司发行,定期分派股利,投资价值较高的股票。
起源于赌场中蓝色筹码最值钱。
在境外注册、香港上市的中国大陆概念的股票。
SpecialTreatment,特别处理股,连续两个会计年度净利润为负,每股净资产低于股票面值(1元)。
项目融资(ProjectFinance):是贷款人向特定的工程项目提供贷款协议融资,对于该项目所产生的现金流享有偿债请求权,并以该项目资产作为附属担保的融资类型。
贷款的还本付息完全依靠经营效益。
要求与项目有利害关系的第三方当事人提供各种担保。
贷款银行有权向担保方追索,以担保金额为限。
传统方式,本质是依靠政府负债。
利:运作简单,速度快,政府信用好。
弊:财政压力大、建设运营责任不清、资金利用效率低下。
Build Operate Transfer
政府特许授权投资公司去建设、运营,在一定期限(如:三十年)后,最终转让给政府。
以特许经营权为主。
利:市场竞争机制、减轻政府财政负担、提高项目运营效率、引入管理与技术;
弊:风险大,因为投资大、期限长、条件差异大、缺乏先例可循。
Build Transfer
政府通过招投标,交给投资者去融资和建设,最后移交给政府。政府按协议分期支付项目投资与回报。
以项目外包为主。
弊:建设费用大、监管难、分包严重、质量得不到保证。
BOOT: Build Own Operate Transfer
BOO: Build Own Operate
BTO: Build Transfer Operate
TOT: Transfer Operate Transfer
Public-Private Partnership
政府与私人部门组成特许经营公司,引入社会资本。
政府补贴PPP项目,社会投资者投资PPP项目。
政府与私人部门风险共担,利益共享。
投资规模达、需求长期稳定、价格调整机制灵活、市场化程度较高的基础设施及公共服务类项目。例如:地铁。
运营管理权与收费收益权分离,将收益权作为基础资产。
按规定,PPP项目资产证券化的基础资产必须追到项目本身,不能以地方政府为基础资产,不能随意承诺保底、安排回购、明股实债等方式担保融资,但可以以财政补贴作为PPP项目收入的来源。
PPP项目期限一般为10~30年,比资产证券化产品期限(多数在7年以内)要长得多。
利:
类型:
证券的发行市场成为一级市场,证券的交易市场称为二级市场。
又称“初级市场”、“一级市场”,是证券发行主体发行和推销新证券所形成的市场。
证券发行者->(中介机构)->投资者,期间受监管者(证监会)监管。
公募公开发行,经过严格审查,因此信用高;可公开交易,因此流动性好;但成本高;
私募不公开发行,不经严格审查,发行程序简单,因此成本低;不公开上市,因此流动性差。
直接发行:自营发行,发行者直接发售证券给投资者;
间接发行:承销发行,发行者委托承销商代为发售证券,承销商收取代理费,并承担发行责任与风险。
主要用于债券发行。
担保发行:发行人以信用或实物担保,承诺证券收益;
无担保发行:不提供任何担保,例如:国债、部分金融债因违约可能性极低,一般无担保。
以美国联邦证券法为代表,遵循公开原则,实质上是发行公司的财务公开制度。如果信息误导,投资者有权起诉。
证券主管机关对证券发行信息资料做审查,不禁止质量差、风险高的证券上市,由市场判断公司价值。
利:政府干预少;流程快;上市成本低。
弊:门槛高,只适用于发达成熟的市场(需要投资者理性,且发行者、承销商等机构恪守法律与职业道德)。
以欧洲各国公司法为代表,实行实质管理原则,发行者必须符合公司法规定的实质条件(经营性质、管理人员资格、资本结构、偿债能力等)。
适用于证券市场历史短、投资者素质不高的地区和国家(欧洲大陆、发展中国家)。
中国大陆 | 中国香港 | 美国 | |
---|---|---|---|
发行上市制度 | 发审制转向核准制 | 高度市场化的核准制 | 注册制 |
审核时间 | 6个月 | 4个月 | 3-4个月 |
审核内容 | 实质审核 | 实质审核 | 形式审核 |
审核标准 | 监管部门严格审核资本结构、性质等 | 按《上市规则》规定指标 | 信息披露真实性 |
承销商收承销费帮发行人销售股票和债券。
全额包销:证券承销商全部购入,然后再转售给投资人;
余额包销:证券承销商按发行额,在发行期限内向投资人发售证券,到期未售出的证券由承销商负责认购。
承销期结束时,将未售出的证券全部退还给发行人。
Initial Public Offerings
IPO包含几个阶段,各阶段可按需并行:
SEO, Seasoned Equity Offerings
原股东无需缴付股款即可获得新股。
通常目的是调整资本结构或将积累资本化。
形式有:
按比例同时进行有偿和无偿增资。
影响因素:
债券的发行需要评级
也称“二级市场”,是已发行的证券在证券市场上买卖的活动。
证券交易包含:
证券交易所,会员资格才可交易,信息及时披露。二级市场中的第一市场。
券商自愿组织的社会团体,会费共担,不以营利为目的。
会员既有交易权,也有交易所的所有权。
案例:上海证券交易所、深圳证券交易所
利:
弊:
商行、券商、信托等企业共同出资建立,以盈利为目的的公司法人。
案例:西方发达国家、中国香港都是公司制,20世纪90年代,为全球主要交易所采用。
交易权与所有权分离,会员无需拥有交易所所有权,也可拥有交易权。只有经过注册的券商才能进入交易大厅直接参加买卖。
利:
弊:
OTC, Over the Counter
在证券交易所以外,由证券买卖双方直接议价成交的市场。
最早形成,公开但未上市发行的证券,如:地方债、市政债、公司债。
已上市证券在交易所以外进行交易的市场,因75年后允许交易所会员自行决定佣金,第三市场发展放缓。
节约交易所内大宗交易的高昂佣金。
买卖双方不经过经纪人,而是通过网络直接大宗交易。
利:成本低;速度快;保密;不冲击证券市场。
弊:给金融监管带来挑战。
起源于政府债券。
随股份公司涌现和信用活动开展而发展。
特点:
我国资本市场结构:
报价制度,做市商制度,主要用于柜台市场;
指令驱动制度,竞价方式,主要用于交易所。
做市商(market maker)制定买价(bid price)/卖价(askprice),在买卖双方中间赚取差价。
案例:纽约证券交易所
信息综合能力强,价格竞争性差,高额利润,易于监管
多元做市商制度,案例:纳斯达克交易所
竞争使市场活跃,交易量增加。做市商信息分散,无垄断地位,交易利润少。
竞价市场,买方订单和卖方订单通过经纪商进入市场,交易中心以买卖双向价格为基准进行撮合。
在一定时段内累积订单,到一定时刻再撮合定价。(通常是开市前10分钟)
在交易日各个时刻连续进行,只要存在匹配订单,交易即发生。
在做市商制度中引入竞价交易制度,如:1997年后的纳斯达克;
在竞价交易制度中引入做市商制度,如:1986年后的伦敦交易所。
交易机制 | 做市商市场 | 竞价市场 |
---|---|---|
竞争方式 | 报价驱动 | 指令驱动 |
价格发现 | 无正式程序 | 正式的市场开盘 |
监管 | 直接监管少,靠竞争改进缺陷 | 直接监管 |
竞争 | 做市商之间 | 客户之间 |
优点 | 成交及时;价格稳定;存货机制纠正买卖不均衡;做市商持仓抑制股价操纵 | 透明度高;信息传递快;运行费用低 |
缺点 | 因买卖集中在做市商手中而缺乏透明度;交易成本高;监管成本增加,难度大; | 难以处理大宗交易;冷门股票成交持续萎缩;价格波动剧烈;价格难以维护,容易被操纵 |
量化投资是利用计算机技术,采用数学模型实现投资理念、投资策略的过程。
数学建模+计算机自动化(半自动化)交易。
2006年,欧美有三分之一的股票交易量由算法交易完成。
算法交易核心问题:平衡冲击成本与等待风险。
代表性的被动型算法交易策略:
预测当天交易时间内各时间片的交易比例分布,最小化冲击成本。
标准VWAP:静态预测当天交易分布;
改进VWAP:根据市场价格走势调整交易量。
不预测交易期内成交量的分布,按交易时段的长度加权。
盯住盘口策略,买入按当前最高买价,卖出按当前最低卖价发出限价交易。若交易未完成且成交价远离限价指令,则撤销,并重新循环。
减小实际成交价与目标价的价差,分激进、中性和保守策略。
下单路径选优策略,从做市商、交易所、暗池等路径择优交易。
利用高速计算机,在极短时间内判断有价值信息,先于其他投资者进行交易。例如:利用交易所之间的微小价差,大量地不停地买卖。
买卖双方匿名配对进行大宗股票交易,主要为机构投资者,运作不透明。
机构投资者不希望公开寻找交易对手,而是希望避免市场冲击并保持信息保密(例如:防止被高频交易套利)。不借助公开交易市场,又存在搜寻成本高的问题。
经纪公司组织,收取手续费,为机构投资者提供交易平台。
证券经纪商组织,在内部对自营业务的订单与客户订单之间进行撮合,避免交易所交易的费用。
对冲基金和电子做市商组织,只接受/取消订单,根据发来的订单,决定是否交易。
多家金融机构共同组织,作为二级暗池,处理各家机构内部撮合池未能完成的订单余额的撮合。
金融中介,本质特征是资产集合,汇集资金并投资证券。
成立后,资产组合固定不变,是无需管理的基金;
主要投资固定收益资产组合。不需要主动管理,因此费率低。
汇集大量投资者形成集合投资,基金的资金存于基金托管人,由基金管理人指令管理被托管资金,组合投资于一系列的证券。
封闭式基金 | 开放式基金 | |
---|---|---|
期限 | 5年以上,多数15年 | 无固定存续期 |
规模 | 不变 | 可变 |
价格 | 供求关系 | 净值 |
策略 | 无赎回、无准备金、可长期投资 | 有赎回、有准备金、无法全额用于长期投资 |
激励机制 | 缺乏 | 按总额收取管理费,若业绩差,则资金赎回流失 |
契约型基金 | 公司型基金 | |
---|---|---|
投资者地位 | 受益人,无发言权 | 既是受益人,也是股东(有发言权等股东权利) |
资产运用依据 | 按契约 | 按公司章程 |
融资渠道 | 不能向银行借款 | 公司有法人资格,可以向银行借款 |
运营方式 | 按契约期运作 | 按公司法运作,除非破产清算,否则有永久性 |
资金性质 | 收益凭证 | 股票 |
中国基金以契约型为主,美国则以公司型居多。
私募基金:无需披露信息,监管不严,隐蔽。如美国的对冲基金,采取合伙制度。
银行
基金公司官网
第三方理财平台
证券公司代销
申购价格=基金单位净值+前端费用
赎回金额=赎回总额-赎回费用
资产:股票、债券、存款、应收利息;
负债:应收管理费、应付税收。
管理费
托管费
交易费
其他:审计费、律师费、信息披露费
收益率=(净值增长+收入+资本利得)/初净值
晨星于1984年成立于美国芝加哥。晨星中国2003年在深圳成立。
分为定性与定量评价。
五个关键因素:投研团队、投资方法、基金公司、业绩、费用。
对基金只做同类比较。
每个月进行一次评级,只对三年及以上的基金进行星级评价。
按当期指数的成分股比例购买的基金,追踪指数的变化幅度。
1971年,世界上第一个指数基金出现在美国。
1994~1996年,市场上91%的股票基金收益增长率低于标普500指数,指数基金优势开始显现。
当市场越有效时,被动化管理越有优势。
特点:
交易型开放式指数证券投资基金(Exchange Traded Fund)
跟踪标的指数的变化,且在证交所上市交易的基金。
ETF对应的是一揽子股票,采用实物申赎,而不是一般开放基金的份额申赎。
实物申赎ETF必须以一篮子股票换取基金份额,或者以基金份额换取一篮子股票。
最小申赎单位都是100万基金份额,通常门槛高,由机构投资者直接申赎。
因为实物申赎可以当天换取为股票,而股票可以当天买卖,因此,ETF基金实物申赎可以做到T+0交割。
我国本土创新,上市型开放式基金(Listed Open-ended Fund)。
LOF基金是开放式与封闭式基金功能的结合体:
合格境内机构投资者(Qualified Domestic Institutional Investor)。
在国境内设立,经国内有关部门批准从事境外证券市场的股票、债券等有价证券业务的投资基金。
在基金的保本周期内,投资者可以拿回认购时原始本金。
不表示周期内可以保本,也不保证周期内申购可以保本。保本周期在中国一般为3年,在国外可达7~12年。
一般使用利息或是极小比例的资产从事高风险投资,大多数资金投资于固定资产。
类似私募基金,由基金公司对多个客户提供投资管理服务。
Private placement fund
向特定的对象募集基金份额。
私募基金无需披露信息,监管要求不严,比较隐蔽。
我国规定所有基金必须公募,因此信托成为私募基金的合法化主要渠道。国内的阳光私募基金一般是私募信托证券基金,主要投资二级证券市场,与重点投资一级股权市场的私募股权基金(PE,Private Equity)在投资对象上有所区别。
私募基金一般为“2+20”收费模型,收取2%管理费和20%盈利部分提成。
Hedge Fund
借助复杂资产组合与风险管理手段,投资多种资产,广泛运用杠杆、卖空以及衍生品等交易策略。
美国的对冲基金即一般意义上的私募基金。
对冲基金以合伙人制度为主,仅提供给有限的合格投资者。
本质上是货币基金。
现逐步向各类非标的资产投资发展,包括:土地质押、ABS债券、P2P小额信贷等。
名义利率
真实利率
记通货膨胀率为
名义利率均衡(The Equilibrium Nominal Rate ofInterest)是指当通货膨胀率增加时,投资者会对其投资提出更高的名义利率要求。
费雪公式代表预期通货膨胀率为
持有期收益由两部分组成:
资本利得:投资买卖差价;
股息或红利:如,股票的股息、分红。
持有期收益率(Holding-periodReturn)是给定期限内的收益率。 \[r = HPR = \frac{p_t - p_0 + d}{p_0}\] 其中,
有效年利率(Effective AnnualRate)是一年期投资价值增长的百分比。
一年期总收入(
年化百分比利率(Annual PercentageRate)是对期限小于一年的投资项目,将该投资的总收益率按照单利的形式转化为年收益率形式的利率。
年化百分比利率是年度化的简单利率: \[r(T) = T \times APR\] 例如:半年期国债总收益率\(r(T) =1.63 \%\),其中\(T =0.5\),则转化为年化百分比利率为\(APR =1.63\% \times 2\)。
总收益率、有效年利率、年化百分比利率之间的关系:
连续复利(Continuously CompoundingInterest)是指在期数趋于无限大的极限情况下对应的利率,此时不同期之间的间隔很短,可以看作是无穷小量。
连续复利收益率可以简单理解为当投资期限为无穷小时的年化百分比收益率的值。
连续复利收益率又称为对数收益率(Log Return)。
风险一般可分为:
期望收益是对收益的数学期望: \[E(r) = \sum_s p(s) r(s)\] 其中,
超额收益(ExcessReturn)是在任意一个特定的阶段,风险资产的实际收益率与实际无风险收益率的差值。
美国政府短期国库券(T-Bill)的收益率可被作为无风险收益率。相比之下,更长期的国债,尽管几乎不存在信用风险,但仍然存在货币购买力风险,如:通货膨胀风险。
风险溢价(RiskPremium)是风险资产预期持有期收益与无风险收益的差值。
收益波动性比率(The Reward-to-VolatilityRatio)有如夏普比率(Sharpe Ratio):
算术平均值收益率: \[E(r) = \sum_{s-1}^{n}p(s)r(s) = \frac{1}{n}\sum_{s-1}^{n}r(s)\]
几何平均值收益率: \[TV_n = (1+r_1)(1+r_2)...(1+r_n)\]
\[g = TV^{1/n} - 1\]
可以作为预期收益的估计工具。
实际上历史越久远对现有的预测影响越小,当以过去的时间序列估时计,需要考虑系统发生的变化。
理想情况下,拟合正态分布进行估计,只需依据过往的时间序列求解收益率的均值与方差。
现实中,小概率事件带来风险,收益可能偏离正态分布,此时标准差不再是衡量风险的完美度量工具,夏普比率也不再是评价证券表现的完美度量工具。对正态分布进行修正,需要考虑偏度(skewness)和峰度(kurtosis)。
Yahoo Finance可获取国内外的股票时序数据。
ProtocolError: ('Connection aborted.', ConnectionResetError(104, 'Connection reset by peer'))
。为了解决该问题,可以尝试手动选择链接下载模型资源。在开展涉及自然语言处理的研究中,需要对自然语言数据进行一系列处理,因此需要使用spaCy。
不过近期发现,在使用命令下载spaCy的预训练模型时,会遭遇网络连接重置的问题,导致无法正常使用该工具。
具体报错如下:
1 | (pytorch) shenjiayun@server3 ~/Dev/VisualEntailment $ python -m spacy download en_core_web_sm |
不便分析。
相关issue:
正常情况下,应该使用官方的命令来安装spaCy工具和最匹配的预训练模型。
1 | pip install spacy |
但现在网络连接被重置,因此只能通过手动处理了。
spaCy在GitHub上同步存放了可下载的模型。
This repository contains
releasesof models for the spaCyNLP library. For more info on how to download, install and use themodels, see the modelsdocumentation.
在无法自动安装的情况下,可以手动选择安装指定的.tar.gz
包,例如:
1 | # pip install .tar.gz archive from path or URL |
采用此方法,意味着需要手动翻阅release
中的资源,找出合适的预训练模型。例如,当前最新的en_core_web_sm
模型是
也可通过spaCy官方的链接确定合适的预训练模型版本,以en_core_web_sm
为例:
Available pretrained statistical models for English
执行后可以成功下载和安装预训练模型:
1 | (pytorch) shenjiayun@server3 ~/Dev/VisualEntailment $ pip install https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-2.3.1/en_core_web_sm-2.3.1.tar.gz |
TypeError: __init__() got an unexpected keyword argument 'column'
。本文对问题进行排查并给出解决方案。使用最新版的IPython7.19.0时,发现无法Tab自动补全,且回车后会出现报错,具体情境例如:
1 | (pytorch) shenjiayun@server3 ~/Dev/VisualEntailment $ ipython |
根据常识推断,ipython使用的是jedi作为languagesever实现代码自动补全。当自动补全失灵的时候,那应该和jedi有关。
从长段的报错中,能看到错误定位于jedi/api/__init__.py", line 726, in __init__
,提示TypeError: __init__() got an unexpected keyword argument 'column'
。确实是调用jedi时出现了错误。
由此查jedi的开源项目,发现issue:
IPython(<=7.19) incompatible with jedi 0.18.0 #1714
Relevant traceback reads as follows:
1
2
3
4
5
6
7 File "../venv/lib/python3.8/site-packages/IPython/core/completer.py", line 2029, in _complete
completions = self._jedi_matches(
File "../venv/lib/python3.8/site-packages/IPython/core/completer.py", line 1373, in _jedi_matches
interpreter = jedi.Interpreter(
File "../venv/lib/python3.8/site-packages/jedi/api/__init__.py", line 725, in __init__
super().__init__(code, environment=environment,
TypeError: __init__() got an unexpected keyword argument 'column'
经过确认,jedi所有者表示该问题系ipython作为下游应用的调用问题,待下游应用更新解决。
davidhalter commented6days ago I think we should continue the discussion in
ipython/ipython#12740.IMOthis is an downstream issue and they should just do a new release.
既然ipython目前最新的7.19.0版本无法正确调用最新的jedi0.18.0版本,那就把jedi版本降级到0.17即可。
通过conda检索可用的jedi版本:
1 | conda search jedi |
可见:
1 | (pytorch) C:\Users\jyshen>conda search jedi |
通过conda安装指定版本的jedi:
1 | conda install jedi=0.17 |
再次测试ipython,不再出现该问题。
]]>参考书籍:
朱荣鑫,黄迪璇,张天. Go语言高并发与微服务实战[M].北京:中国铁道出版社有限公司,2020.
虚拟化是云计算的基石。
虚拟化技术成熟,云计算市场出现。
云计算模式:
类型 | 传统IT | IaaS | PaaS | SaaS |
---|---|---|---|---|
应用程序 | × | × | × | √ |
数据 | × | × | × | √ |
运行时 | × | × | √ | √ |
中间件 | × | × | √ | √ |
操作系统 | × | × | √ | √ |
虚拟化 | × | √ | √ | √ |
服务器 | × | √ | √ | √ |
存储 | × | √ | √ | √ |
网络 | × | √ | √ | √ |
容器化本质上是虚拟化的改进。
虚拟化通过Hypervisor分离操作系统,容器化共享操作系统。
LXC(LinuxContainer)侧重容器运行环境的资源隔离和限制,类似进程沙箱,而没有容器镜像打包技术,所以没有普及。
Docker在LXC的基础之上,建立了一套镜像打包和运行机制,将应用程序和依赖项打包成镜像文件,换别的Docker中也能运行,实现Build,Ship and Run。
容器编排技术经过Mesos、Swarm和Kubernetes三家竞争,最后随着Kubernetes的成熟及其与Docker的融合,PaaS技术的主流路线过渡到了KubernetesDocker。2018年,Kubernetes占据统治地位。
企业降低对IT基础设施的直接投入,而是通过上云来获取计算和存储能力,按时按需计费。
云计算降低了IT支出,降低了行业技术壁垒。
移动互联网,业务高速发展,快速迭代。
Pivotal(云原生应用提出者):
CNCF(Cloud Native Computing Foundation):
Missionof the Cloud Native Computing Foundation The Foundation’s mission is to make cloud native computingubiquitous. The CNCF Cloud Native Definition v1.0 says:
Cloud native technologies empower organizations to build andrun scalable applications in modern, dynamic environments such aspublic, private, and hybrid clouds. Containers, service meshes,microservices, immutable infrastructure, and declarative APIs exemplifythis approach.
These techniques enable loosely coupled systems that are resilient,manageable, and observable. Combined with robust automation, they allowengineers to make high-impact changes frequently and predictably withminimal toil.
The Cloud Native Computing Foundation seeks to drive adoption of thisparadigm by fostering and sustaining an ecosystem of open source,vendor-neutral projects. We democratize state-of-the-art patterns tomake these innovations accessible for everyone.
2012年,Heroku提出12-Factors云应用设计理念。
核心思想:
云原生应用利用微服务、服务网络、容器、DevOps和声明式API等代表性技术,来构建容错性好、易于管理和便于观察的松耦合系统。
将明确定义的功能分成更小的服务,服务之间是松耦合的,每个服务可以独立迭代。
优点:降低系统复杂度、独立部署、独立扩展、跨语言编程。
缺点:需要构建、测试、部署、运行数十个独立的服务,支持多种语言和环境,还引入了分布式系统的复杂性,如:网络延迟、容错性、消息序列化、不可靠网络、异步机制、版本化和差异化。
将微服务和所需的所有配置、依赖关系和环境变量打包成容器镜像,轻松移植到新的服务器节点。
人力运维部署成本太大,在Docker基础之上,引入Kubernetes可以实现容器集群的自动化部署、自动扩缩容和维护等功能。
Kubernetes不仅支持Docker,还支持Rocket等其他容器技术。
微服务技术架构有:
服务网络(Service Mesh)对运行于其上的云原生应用是透明的。
服务网格是处理服务间通信的基础设施层。它负责构成现代云原生应用程序的复杂服务拓扑来可靠地交付请求。在实践中,ServiceMesh通常以轻量级网络代理阵列的形式实现,这些代理与应用程序代码部署在一起,对应用程序来说无需感知代理的存在。
开源的服务网络软件:Istio、Linkerd、Envoy、Dubbo Mesh等。ServiceMesh可以运行在Kubernetes上。
DevOps包含三个部分:
DevOps
云原生将云目标从节约IT成本转向推动业务增长。
巨石(Monolith)应用,易于测试、部署,但编译慢、局部改动就要重新部署、技术难扩展。
对单体架构垂直拆封,例如:用户界面层、业务逻辑层、数据访问层。
每个服务登记到服务登记中心上。
服务消费者从服务登记中心寻找,通过发送消息由企业服务总线(EnterpriseService Bus)转换后发送给相应的服务来调用服务。
SOA是中心化架构,关注系统集成。
大型复杂软件有一个或多个微服务组成。微服务可独立部署、松耦合、仅关注完成单一职责。每个职责代表一个高内聚的业务能力。
微服务是去中心化架构,关注分散管理、代码重用、快速扩展。
微服务架构的特点:
如果拆分的服务过多,服务治理成本会极大升高,开发调试成本高。服务之间相互依赖,还可能形成复杂依赖链,异常时出现雪崩效应。
代表技术:
四要素:
云原生架构依托PaaS产品:
Spring Cloud将各家公司开发的比较成熟的服务框架组合起来,通过SprIngBoot风格再封装,屏蔽复杂配置和实现原理,对外提供简单易懂的工具包。
Dubbo框架是分布式服务框架,提供RPC方案和SOA服务治理方案,特点主要在:远程通信、集群容错、自动发现。
Go-kit(gokit.io)是Go语言工具包的集合。
Go-kit不仅是微服务工具包,也非常适合构建优雅的架构设计。
Go-kit应用程序架构:
Go Micro是Go语言实现的插件化RPC微服务框架,包含组件:
Domain Driven Design
分为4层:
业务系统
限界上下文和子域一一对应,一个限界上下文只使用一套通用语言,并保证其清晰简洁。
实际情况中,根据业务,有时将多个界限上下文合并。
随着微服务架构流行,组织内部产生许多小规模团队。组织架构从层级职能组织变成扁平的小团队集群。
]]>职业生涯不是短跑比赛,职业生涯的持续时间长的惊人,可分三个阶段:
成功的可持续职业生涯是靠职场燃料推动的。
积累、不断更新并精明地消费职场燃料。
基本地职场燃料:
明智之举是培养情商、创造力、协作能力和建立信任关系的技能。
像领英这样的在线平台将成为公司寻找人才、个人寻找工作的主要场所。
创业和自由职业将在不久的将来蓬勃发展,工作目标也将更多样化。
退休并不代表就能安享晚年,继续工作才能获得稳定的收入。
想在工作中更快乐,就需要提高幸福感。
]]>GIL(Global InterpreterLock)指的是全局解释器锁,由CPython解释器引入。因为CPython解释器的内存管理是线程不安全的,所以为了避免多线程同时执行Python字节码造成线程安全问题,就加了这么一个全局的互斥锁。可也正是因为这个全局互斥锁,导致Python的多线程实际上同时只有一个线程在运行,显然无法充分利用多处理器的性能。
参考官方解释:
Python has one peculiarity that makes concurrent programming harder.It’s called the GIL, short for Global Interpreter Lock.The GIL makes sure there is, at any time, only one thread running.Because only one thread can run at a time, it’s impossible to usemultiple processors with threads. But don’t worry, there’s a way aroundthis.
The GIL was invented because CPython’s memory management is notthread-safe. With only one thread running at a time, CPython can restassured there will never be race conditions.
DistributedDataParallel(DDP)相较于DataParallel(DP)有诸多优势,包括功能上的优势和性能上的优势:
功能上:
性能上:
总的来讲,功能上的优势其实也是为了更好利用设备性能。
原理可参阅:
另有一篇2020年的论文:
Li S, Zhao Y, Varma R, et al. PyTorch distributed: experiences onaccelerating data parallel training[J]. Proceedings of the VLDBEndowment, 2020, 13(12): 3005-3018.
总的架构可以参考:
在这样的架构中,有如下术语和数值:
相较于DataParallel只需要简单地套到原模型上,DistributedDataParallel因为其原理是基于多进程的,因此写起来会稍微显得复杂一点点。
1 | """ |
1 | def launch(args): |
launch
使用multiprocessing.spawn
来快速创建nprocs
个新进程,每个进程都执行worker
函数,并传入args
作为函数参数。
需要注意的是,spawn
默认会为函数传入一个i
,且i
在[0, nprocs)
之间。即,worker
函数收到的参数列表是(i, args, )
。
每一个worker进程做的工作分以下几个阶段:
DistributedSampler
,以便DataLoader将数据加载给每个GPU上训练的模型。参阅torch.distributed的官方文档:
Distributedcommunication package - torch.distributed
初始化函数原型:
1 | torch.distributed.init_process_group(backend, init_method=None, timeout=datetime.timedelta(0, 1800), world_size=-1, rank=-1, store=None, group_name='') |
其中,进程组后端是负责提供进程组聚合通信(collectivecommunication)的库。PyTorch支持Gloo, MPI和NCCL三种,推荐的做法是,
参考资料:
Gloo is a collective communications library. It comes with a numberof collective algorithms useful for machine learning applications. Theseinclude a barrier, broadcast, and allreduce.
The NVIDIA Collective Communication Library (NCCL) implementsmulti-GPU and multi-node communication primitives optimized for NVIDIAGPUs and Networking. NCCL provides routines such as all-gather,all-reduce, broadcast, reduce, reduce-scatter as well as point-to-pointsend and receive that are optimized to achieve high bandwidth and lowlatency over PCIe and NVLink high-speed interconnects within a node andover NVIDIA Mellanox Network across nodes.
初始化可以选择通过init_method
填写通信地址和端口,也可以通过store
来传入一个进程间共同访问的键值对容器。
1 | # initialize with TCP in this example |
例子中演示了基于init_method
的通信方式,具体采用TCP连接的方式来初始化,也可通过环境变量的方式(见注释掉的代码)。另外,还可以使用共享的文件系统来实现初始化,可参阅torch.distributed
的官方文档。我觉得TCP连接足够简单且兼容性好,这里就以TCP的方式为主了。
参阅torch.nn.parallel.DistributedDataParallel的官方文档:
torch.nn.parallel.DistributedDataParallel
首先,需要注意的是,在建立DDP之前,在N个GPU的机器上,spawn出N个进程的时候,需要确保每个进程负责其对应的那一个GPU,不要互相打架。
1 | # ensuring that each process exclusively works on a single GPU |
用torch.nn.parallel.DistributedDataParallel
类包装原模型,并将该进程的模型映射到对应的GPU设备上。
参阅torch.utils.data.distributed.DistributedSampler的官方文档:
torch.utils.data.distributed.DistributedSampler
其实就是在多进程的情况下,每个进程训练数据集的一个子集,不应互相重复,通过DistributedSampler来实现分布式的采样原数据集中的一个子集:
1 | # dataset |
需要注意的是,多epoch场景下,需要在每个epoch开始前用sampler.set_epoch(epoch)
设置当前的epoch,以免每次epoch训练的数据顺序都是相同的。
根据PyTorch的torchvision库的文档,FasterR-CNN模型对象可以直接通过fasterrcnn_resnet50_fpn
函数来构造。
具体地,官方文档给出了训练时和预测时的调用样例:
1 | True) model = torchvision.models.detection.fasterrcnn_resnet50_fpn(pretrained= |
fasterrcnn_resnet50_fpn
函数在torchvision.models.detection.faster_rcnn
包中实现,文档见
1 | def fasterrcnn_resnet50_fpn(pretrained=False, progress=True, |
该函数的实现中,首先进行参数检查:
trainable_backbone_layers
参数,必须在0~5之间,表示从最后一层开始计数,有几层在训练中是可优化的;pretrained
和pretrained_backbone
参数,如果整个模型都设为预训练的,那就当然没必要再单独下载预训练的backbone
了,把整个FasterR-CNN模型都载入预训练参数即可。FasterR-CNN模型是FasterRCNN
类的实例。实例化时,传入指定的backbone
作为FasterRCNN
的backbone。
backbone通过对外开放的resnet_fpn_backbone
函数来构造。
resnet_fpn_backbone
函数在torchvision.models.detection.backbone_utils
包中实现。
1 | def resnet_fpn_backbone( |
首先,根据传入参数选出对应的resnet模型进行实例化。
随后,检查trainable_layers
参数的合法值范围,并通过parameter.requires_grad_(False)
来freeze除此以外的其他层。
默认未定义extra_blocks
的时候,会在featuremap结尾添加一个maxpool2d层,该LastLevelMaxPool
类实现并不复杂:
1 | # defined in torchvision.ops.feature_pyramid_network |
根据官方文档F.max_pool2d(x[-1], 1, 2, 0)
表示:
x[-1]
;关于卷积类的操作可以结合可视化理解:
Convolutionarithmetic
然后,处理其他传参:
return_layers
,这是一个dict,与传入的backbone相配合,key是backbone的modulename,value是用户定义的返回名;in_channels_list
,这是一个list,与传入的backbone和return_layers
相配合,是backbone返回的每一层featuremap的通道数;out_channels
,一个整数,FPN中的通道数。我们以一个例子贯穿始终:
1 | import torch |
我们使用预训练模型,并模拟输入两张图片。均为3通道,一张
FasterRCNN
类在torchvision.models.detection.faster_rcnn.py
中实现。
1 | class FasterRCNN(GeneralizedRCNN): |
FasterRCNN
的代码看起来很长,实际上主要是文档注释。
FasterRCNN
的实现只有__init__
函数,因为FasterRCNN
继承自GeneralizedRCNN
,主要结构和计算流的实现都在父类中实现了,该子类的实现实际上只需要做一些参数检查和子类的具体子结构的实例化。
FasterRCNN的__init__
函数的主要就是在做参数检查和一些实例化准备工作,其结果就是将准备好的backbone、rpn、roi_heads和transform对象传递给父类(GeneralizedRCNN)的初始化函数,由此构建一个FasterRCNN实例对象。
GeneralizedRCNN
在torchvision.models.detection.generalized_rcnn.py
中实现,负责以父类的形式定义RCNN架构的整体计算。
1 | class GeneralizedRCNN(nn.Module): |
在__init__
中,GenerailizedRCNN把R-CNN架构定义为4个组成部分:
transform
:一个变换模型,用于对图像和其他输入进行变换;backbone
:一个特征提取模型,输入的是进过变换处理的图像张量,输出的是取得的图像特征features;rpn
:一个RPN模型,输入包含——图像images、backbone提取出的图像特征features以及训练时输入的包含bboxground truth的targets,输出包含——预测的区域proposals和相应的损失;roi_heads
:一个RoIHeads模型,输入包含——backbone输出的features,RPN输出的proposals,以及图像尺寸和训练时的targets。该类的__forward__
计算流差不多就是这四部分依次执行的过程,除了一些参数检查,训练时和预测时对输入的区分以外,主要代码逻辑可以概括为:
1 | def forward(self, images, targets=None): |
FasterR-CNN模型对输入图像的预处理由torchvision.models.detection.transform
包的GeneralizedRCNNTransform
类实现。
1 | class GeneralizedRCNNTransform(nn.Module): |
对输入图像的初步转换处理在forward
前向传播函数中实现,主要实现normalize和resize操作:
self.normalize
:初始参数在FasterRCNN的初始化中被设为image_mean = [0.485, 0.456, 0.406]
和image_std = [0.229, 0.224, 0.225]
;self.resize
:初始参数在FasterRCNN的初始化中被设为min_size=800, max_size=1333
;self.batch_images
,对一个batch的图像做了Padding,使其输出的张量尺寸一致。根据该转换模块的默认值,结合本节开头的例子:
BackboneWithFPN
在torchvision.models.detection.backbone_utils
中实现,其作用就是以ResNet模型中提取出的一些中间层作为backbone,在backbone后面继续接上一个FPN。
1 | class BackboneWithFPN(nn.Module): |
该类的实现很简单,就像是一个组合,把backbone和FPN装起来:
然后数据流定义很简洁,就是输入数据x先经过body,再经过fpn,就完成了。
有了backbone+FPN的模型,就可以进一步构造Faster R-CNN模型了。
torchvision
的FasterR-CNN的backbone负责提取图像特征,具体实现由ResNet中间层衔接FPN组成。
class ResNet(nn.Module)
ResNet在torchvision.models.resnet
包中实现,属于卷积神经网络实现的范畴,本文不再赘述。
有了resnet作为backbone,就可以通过resnet_fpn_backbone
构造一个在resnet后面接上FPN的模型,具体地,是构造BackboneWithFPN
类的对象。
torchvision
实现中:
本节的例子经过ResNet部分的计算后,从输入的
'0': shape[2, 256, 256, 272]
,源自ResNet的layer1;'1': shape[2, 512, 128, 136]
,源自ResNet的layer2;'2': shape[2, 1024, 64, 68]
,源自ResNet的layer3;'3': shape[2, 2048, 32, 34]
,源自ResNet的layer4;FeaturePyramidNetwork
在torchvision.ops.feature_pyramid_network
包中实现。FPN实现了金字塔结构的特征提取,低层的卷积感受野小,其特征代表小目标的特征,而高层的卷积感受野大,因此其特征适合表示大目标特征。在目标检测中运用FPN,在低层配合小尺寸anchor,在高层配合大尺寸anchors,有利于同时有效检测小目标和大目标。
1 | class FeaturePyramidNetwork(nn.Module): |
按原论文的思路,FPN第n层输出feature map
此后,采用3×3卷积对合并后的featuremap进行卷积处理,以便消除上采样操作造成的失真效应(aliasingeffect)。
此时,形成的每层的最终的feature map就是最终的feature map
在torchvision
的具体实现中:
self.inner_blocks
就是FPN的所有1×1卷积;self.layer_blocks
就是FPN合并后需要用到的3×3卷积;这两者都是nn.ModuleList()
,在__init__
初始化时,在一个n次(n个featuremap)的for循环中进行初始化,都填入nn.Conv2d
对象,设置为统一的out_channels
。
在__forward__
定义的计算流中,核心代码逻辑可以概括为:
1 | def forward(self, x: Dict[str, Tensor]): |
具体步骤是从后往前计算每一层的result,即论文中的
inner_lateral
就是CNN的featuremap经过1×1卷积计算的结果,该卷积通过self.get_result_from_inner_blocks(x[idx], idx)
实现;inner_top_down
就是从后一层F.interpolate(last_inner, size=feat_shape, mode="nearest")
;last_inner
就是两者合并的结果,通过element-wiseaddition实现;results
前,还需要用3×3卷积计算一下,即self.get_result_from_layer_blocks(last_inner, idx)
。最后,如果还有额外计算块的话,就再算一遍,取得这层的结果也加入。
在具体实现中,在FPN尾部增加了LastLevelMaxPool
,并将其计算结果命名为pool
加入了names
。
本节的例子经过FPN部分的计算后,从ResNet输出的4个通道数不同的featuremaps,转换为了各层通道数一致的一个有序字典OrderedDict:
'0': shape[2, 256, 256, 272]
,源自ResNet的layer1;'1': shape[2, 256, 128, 136]
,源自ResNet的layer2;'2': shape[2, 256, 64, 68]
,源自ResNet的layer3;'3': shape[2, 256, 32, 34]
,源自ResNet的layer4;'pool': shape[2, 256, 16, 17]
,源自FPN作为extra_blocks
的LastLevelMaxPool
。RegionProposalNetwork
在torchvision.models.detection.rpn
包中实现。
RegionProposalNetwork
的实现比较长,主要看__init__
和__forward__
就可以了。
1 | class RegionProposalNetwork(torch.nn.Module): |
主要看__forward__
中的计算流,RPN的完整过程
self.head
)中,输出object/non-object分类分值(objectness
)和bbox回归数值(pred_bbox_deltas
);self.anchor_generator
为当前输入的图像和featuremap生成anchors
;self.box_coder.decode
把bbox回归数值pred_bbox_deltas
算到锚框anchors
上,得到预测出的候选框proposals
;proposals
可能很多且相互密集重叠,那么就通过self.filter_proposals
做一遍过滤,输出候选框proposals
和与之对应的分值scores
;proposals
与候选框真值之间的误差来计算损失。AnchorGenerator
在torchvision.models.detection.anchor_utils
中实现,其作用是根据预定义的anchor的sizes和aspect_ratios,针对图像到featuremap的尺寸比例,计算feature map对应的anchors。
1 | class AnchorGenerator(nn.Module): |
这部分代码也有点长,主要原理也还是看__forward__
计算流即可,这里面有一系列预备的计算,随后就是两层的for循环,表示:每一个图片可以传入
两层循环,外层遍历images,内层遍历featuremaps,由此输出所有图片的feature map上的anchors。
在具体实现中,AnchorGenerator:
self.set_cell_anchors
函数负责为每一层featuremap生成self.cell_anchors
,这个CellAnchors的尺寸基于的是输入图片tensor的尺寸,;self.cached_grid_anchors
函数内会进一步调用self.grid_anchors
函数,该函数负责根据featuremap的网格尺寸以及该featuremap相较于输入图片tensor的步长,计算出anchors_over_all_feature_maps
,它的尺寸则是基于输入图片tensor的尺寸。torch.cat
拉平每个图片上不同feature map上的所有对应到例子,torchvision
实现默认为:
因此,cell_anchors
中,每层feature map都是3个anchorcells:
'0'
: shape[3, 4]'1'
: shape[3, 4]'2'
: shape[3, 4]'3'
: shape[3, 4]'4'
: shape[3, 4]结合例子来算,把cell_anchors
算到输入图像张量的每一个滑窗位置上,就可以算出所有位置上的所有anchors_over_all_feature_maps
:
'0'
: shape[208896, 4],'1'
: shape[52224, 4],\(52224= 128 \times 136 \times 3\);'2'
: shape[13056, 4],\(13056= 64 \times 68 \times 3\);'3'
: shape[3264, 4],\(3264 =32 \times 34 \times 3\);'4'
: shape[816, 4],\(816 =16 \times 17 \times 3\);最后返回的anchors
会为输入的每个图片复制一份,并通过torch.cat
拉平:
RPNHead
在torchvision.models.detection.rpn
包中实现。RPNHead被用于以滑窗的形式在特征提取出的featuremap上滑动并计算每个anchor的bbox回归值和object/non-object二分类。
1 | class RPNHead(nn.Module): |
可以看到,RPNHead的结构不复杂,就是三个卷积:
self.conv
:3×3卷积,对输入的featuremap做卷积处理;self.cls_logits
:1×1卷积,对处理后的feature map self.bbox_reg
:1×1卷积,对处理后的feature map 在forward
前向传播计算的时候,输入的x是一个List[Tensor]
,即FPN的输出。值得注意的是,for
循环遍历的并不是每一张图片,而是FPN输出的每一层特征。
在本例中,RPNHead的两个卷积分支输出了两个List[Tensor]
:
logits
(objectness
):
'0'
: shape[2, 3, 252, 272];'1'
: shape[2, 3, 128, 136];'2'
: shape[2, 3, 64, 68];'3'
: shape[2, 3, 32, 34];'4'
: shape[2, 3, 16, 17];bbox_regs
(pred_bbox_deltas
):
'0'
: shape[2, 12, 252, 272];'1'
: shape[2, 12, 128, 136];'2'
: shape[2, 12, 64, 68];'3'
: shape[2, 12, 32, 34];'4'
: shape[2, 12, 16, 17];因为每个滑窗位置对应三种ratios
,即3个anchors,所以logits
是3个值,而bbox_regs
因为坐标乘4,所以是12个值。
BoxCoder
在torchvision.models.detection._utils
中实现。
1 | class BoxCoder(object): |
RPN中self.box_coder
使用BoxCoder作为bbox的编解码器:
1 | proposals = self.box_coder.decode(pred_bbox_deltas.detach(), anchors) |
通过BoxCoder实例,将RPNHead回归出的pred_bbox_deltas
与RPN的锚框anchors
做解码计算,把回归出的偏移值加到基准anchors位置上,解码输出候选框proposals
。
在本例中,RPN的forward
对解码出的原始proposals
做了维度整理proposals = proposals.view(num_images, -1, 4)
,得到的proposals
是:
filter_proposals
是一个对RPNHead生成的候选框proposals
的过滤操作,在RPN类RegionProposalNetwork
中作为成员函数实现。
1 | class RegionProposalNetwork(torch.nn.Module): |
对proposals
的过滤操作分几个阶段实现:
objectness
分值和num_anchors_per_level
来在每层选出top_n_idx(pre_nms_top_n)
用于在NMS前先筛选一下proposals
;post_nms_top_n
的目标作为返回结果。最后将这么多筛选操作筛选出的final_boxes
和final_scores
返回(boxes
是筛选后的proposals
,scores
是筛选后的objectness
)。
在本例中,有两张图片,每张图片上有278256个anchors
,因此产生278256个proposals
和objectness
,进过筛选处理后:
final_boxes
:final_scores
:因为FasterRCNN中默认值rpn_post_nms_top_n_test=1000
,所以在eval模式(即test,infer情况)下,例子中的两张图片都各筛选出了top-1000个boxes。
RoIHeads
在torchvision.models.detection.roi_heads
包中实现。
1 | class RoIHeads(torch.nn.Module): |
主要看__forward__
函数的实现,虽然很长,但是如果只考虑FasterR-CNN需要的部分(不考虑用于MaskR-CNN的图像分割分支),其实可以概括为:
1 | def forward(self, |
Faster R-CNN的RoIHeads主要包含几个步骤:
box_features = self.box_roi_pool(features, proposals, image_shapes)
执行,FasterR-CNN的RoIPool的具体实现是torchvision.ops.poolers
包中的MultiScaleRoIAlign
类。因为目标的形状不尽相同,所以涉及到的特征窗口就不尽相同。RoIPool的目的在于通过把尺寸不定的RoIwindow划分为固定的网格做池化,来把输入的变长的RoI特征池化为定长的特征输出,方便后续的特征处理。box_features = self.box_head(box_features)
执行,FasterR-CNN的MLPHead的具体实现是torchvision.models.detection.faster_rcnn
中的TwoMLPHead
类。MLPHead承接RoIPool池化出的定长特征向量,并通过MLP做非线性计算,输出最终特征用于后续的任务(分类、回归等)。class_logits, box_regression = self.box_predictor(box_features)
执行,FasterR-CNN的Predictor的具体实现是torchvision.models.detection.faster_rcnn
中的FastRCNNPredictor
类。上一步MLP操作输出的特征作为最后的特征,交给Predictor去做具体任务的预测,例如:目标分类,bbox位置和尺寸值的回归预测。boxes, scores, labels = self.postprocess_detections(class_logits, box_regression, proposals, image_shapes)
,该函数是RoIHeads类的一个成员函数。torchvision
采用torchvision.ops.poolers
包中的MultiScaleRoIAlign
作为FasterR-CNN的RoI Pool的实现。
1 | class MultiScaleRoIAlign(nn.Module): |
实际上Faster R-CNN论文发表时并没有RoI Align技术,当时仍然沿用的是FastR-CNN中的RoI Pool。RoIPool指的是对RoI内的特征做池化,取得一个小的featuremap,即,把原来形状不定的\(h \timesw\)(\(h,w\)均为变量)的RoI窗口内的特征池化为统一的
RoI Align其实是Mask R-CNN论文中提出的概念。RoI Align觉得RoIPool的处理太粗糙了,存在量化(Quantization)的问题,计算featuremap上的窗口坐标的时候就舍入取整了,窗口内划分bins的时候又舍入取整了,这样就很不精确。这样的量化处理,用作分类任务倒还影响不大,但是用作图像分割这种像素级精度的任务时就是个问题了。
RoI Align对RoIPool的改进及其二次插值的数学计算原理可以仔细阅读这篇文章:
UnderstandingRegion of Interest — (RoI Align and RoI Warp) | by Kemal Erdem(burnpiro) | Towards Data Science
MultiScaleRoIAlign核心的RoIAlign操作是通过调用torchvision.ops.roi_align
包的roi_align
函数实现的,而该函数实际上也只是执行了对底层torch.ops.torchvision.roi_align
函数的调用。
在本例中,输入的两张图片经过RPN处理后,各得到1000个boxes,即共2000个boxes。经过RoIPool/ RoIAlign处理后,输出为:
box_features
(results
):shape[2000, 256,7, 7]表示2000个boxes,都被池化为了\(C, H, W =256, 7, 7\)的特征。
torchvision
采用torchvision.models.detection.faster_rcnn
包中的TwoMLPHead
作为MLPHead的实现。
1 | class TwoMLPHead(nn.Module): |
这部分并不复杂,实际上就是实现了两层的MLP,名为fc6
和fc7
。
在本例中,RoIAlign输出的box_features
原shape[2000, 256,7, 7],在TwoMLPHead中:
返回的是如上非线性转换后的box_features
特征,此时shape[2000,1024]。
torchvision
采用torchvision.models.detection.faster_rcnn
包中的FastRCNNPredictor
作为Predictor的实现。
1 | class FastRCNNPredictor(nn.Module): |
这部分也并不复杂,实际上就是同论文中描述的一样,通过MLP实现了两个预测分支:
self.cls_score
分支预测目标的分类分值scores
;self.bbox_pred
分支回归目标对应各个分类的目标框回归值。在torchvision
的预训练模型中,FastRCNNPredictor的num_classes
是91,即能识别含背景在内的91个类。
在本例中,两个分支根据RoIAlign和TwoMLPHead提取出的特征,分别预测输出:
class_logits
(socres
):shape[2000,91];box_regression
(bbox_deltas
):shape[2000,364]。意思是输入的2张图片上共2000个框(1000个/图片),这2000个框都做了分类预测,并且为每个类分别计算了目标框的回归修正值。
在模型的Predictor完成预测后,还需要做后续的一些处理,该部分的处理在torchvision.models.detection.roi_heads.RoIHeads
的postprocess_detections
函数中实现。
1 | class RoIHeads(torch.nn.Module): |
后处理在for循环中,遍历每一张图片:
self.detections_per_img
个的top-k个目标。因为本例输入的是随机值填充的模拟图片,所以在去除低分框的环节,2000个候选框就因为没有实际的目标而被全部滤除了。
FasterR-CNN模型的后期处理由torchvision.models.detection.transform
包的GeneralizedRCNNTransform
类实现。
1 | class GeneralizedRCNNTransform(nn.Module): |
其实对于目标检测而言,实际上只对boxes
的坐标做了resize的操作。因为GeneralizedRCNNTransform
在对输入图像做预处理的时候,有进行尺寸转换,而且转tensor的时候又增加了padding是同一batch的图像张量能够保持尺寸一致。所以输出结果的时候,还是要把在tensor上的坐标转换为原始图像尺度上的坐标。
最后总览一下整个模型的实现结构,只需通过简单的print
:
1 | print(model) |
,即可输出结果:
1 | FasterRCNN( |
模型结构可以总结为层次结构:
参考书籍:
朱荣鑫,黄迪璇,张天. Go语言高并发与微服务实战[M].北京:中国铁道出版社有限公司,2020.
代码同步更新在GitHub:
1 | package main |
1 | package main |
1 | package main |
1 | package main |
1 | package main |
1 | package main |
1 | package main |
1 | package main |
1 | package main |
1 | package main |
1 | package main |
1 | package main |
1 | package main |
1 | package main |
1 | package main |
1 | package main |
另
1 | package main |
1 | package main |
在Linux环境下重装NVIDIA驱动时,出现报错,原因是内核模块正在使用中kernel module (nvidia_modeset) in use
,导致无法安装新驱动。
NVIDIA驱动安装的报错页面给出的解决方案是重启一下(reboot)即可。但如果是服务器环境下,有其他用户的计算任务在执行,不希望打断,能否避免重启呢?
不知道原因的情况下,直接使用rmmod nvidia_modeset
卸载该内核模块时,会遭遇报错,因为正在被占用而导致无法卸载。而rmmod -f
的强制卸载又存在风险,可能造成系统崩溃(systemcrash)。
照理说,老驱动已经卸载,那么不应该存在驱动相关的内核模块仍被使用的情况。
根据提示,既然是内核模块被占用的问题,那首先通过lsmod
检查内核模块的使用情况,可以查到类似的引用关系:
1 | Module Size Used by |
从中可以发现,内核模块nvidia_modeset
依赖于内核模块nvidia
。
通过进一步检查nvidia相关进程ps -aux | grep nvidia
,发现实际上是nvidia的persistencemode的守护进程占用了内核模块nvidia_modeset。而之所以有这样一个守护进程,是为了避免nvidia-smi每次唤起过慢的问题,即,通过设置sudo nvidia-persistenced --persistence-mode
启用persistencemode,借助守护进程来维护记录GPU的状态,避免每次nvidia-smi都需要同步检查每一个GPU状态在阻塞等待上耗费太多时间。
查出了原因,再想办法解决就容易了。
首先,通过ps -aux | grep nvidia
找出使用nvidia_modeset
的进程。
随后,通过sudo kill [pid]
结束该persistencemode的守护进程。
通过ps
进行验证,等待进程结束后,再检查lsmod
就可以发现nvidia_modeset
不再被占用了。
此时,通过rmmod
卸载残余的nvidia内核模块,就不会再有报错了。
如此清理完内核模块后,重新执行NVIDIA驱动安装程序,即一切正常了。
]]>钱文品. Redis深度历险:核心原理与应用实践[M]. 北京: 电子工业出版社,2019.
Redis主要可以用作:
1 | # ubuntu |
1 | # redis command-line interface |
Redis提供5种基础数据结构,分别为:字符串string、列表list、字典hash、集合set、有序集合zset。
Redis所有数据结构都以唯一的key字符串作为名称,以此获得相应的value数据。
Redisstring内部数据结构类似Java的ArrayList,预分配冗余空间以免频繁分配内存。当字符串小于1MB时,扩容方法为加倍当前容量;当超过1MB时,每次扩充1MB空间。字符串最大长度为512MB。
Redislist类似Java中的LinkedList链表(实际上不完全是),双向链表,插入和删除时间复杂度O(1),查询时间复杂度O(N)。
当list删除最后一个元素时,该数据结构被自动删除,内存回收。
双向链表可以被用来实现队列、栈。
Redis list底层实现是quicklist数据结构。
当list元素较少时,采用ziplist(压缩列表)。ziplist用连续内存将所有的元素连续存储。
当list元素较多时,采用quicklist(快速链表)。quicklist是将链表与ziplist结合的产物,每一个ziplist包含多个元素,却仅需两个前后指针,因此,quicklist避免了为每个元素配备prev/next双指针的空间消耗。quicklist既满足了快速的插入和删除,又避免了产生较大的空间冗余。
Redis hash类似Java中的HashMap,无序字典,存储键值对。
hash采用数组+链表的数据结构,但hash的值只能是字符串。
当hash删除最后一个元素时,该数据结构被自动删除,内存回收。
Java的HashMap每次rehash需要一次性全部rehash,而Redis的hash在rehash时,为了避免阻塞服务,采用渐进式rehash。
渐进式rehash在rehash时,保留新旧两个hash结构。旧的hashtable仍可用作查询,同时将旧的hashtable持续rehash到新的hashtable上。等rehash全部完成后,才以新的hashtable取代旧的hashtable。
Redis set相当于Java中的HashSet,内部的键值对时无序的、唯一的。
set的底层实现相当于是hash,只不过hash的value村的都是NULL。
当set删除最后一个元素时,该数据结构被自动删除,内存回收。
Redis zset类似Java的SortedSet和HashMap的结合体。
zset一方面是set,value元素是唯一的,另一方面其有序性是依靠为value赋予score作为排序权重实现的。
当zset删除最后一个元素时,该数据结构被自动删除,内存回收。
zset内部的排序功能采用skiplist实现。
skiplist中,高层(level)链表跨度大,连接比较大的跨度范围。越往底层跨度越小,表示比较小的跨度范围。通过skiplist,可以从大范围缩小到小范围,快速定位插入与查询的位置。
list, set, hash, zset这四种容器数据结构具有两个通用性质:
所有数据结构都可设置过期时间,过期则删除。
字符串设置过期时间后,如果字符串被修改,则过期时间失效。
setnx (set if not exists)指令做锁标记,del删除锁标记。
1 | > setnx lock:resource_a true |
list可以作为异步消息队列。
rpush/lpush操作入队列,lpop/rpop操作出队列。
blpop/brpop可以阻塞式(blocking)地读取数据。
get/set处理整个位图的内容。
getbit/setbit处理各个位。
bitcount统计范围内1的位数。
bitpos查询第一个0或1的位置。
bitfield,包含get/set/incrby子指令,可以读取、设置和自增指定范围的位。bitfield可以混合多个子指令执行。
统计PV量无需去重,incrby自增就可以。统计UV则需要去重,不是简单的自增,去重常用的set集合在数据量很大时会消耗巨大的内存空间。
HyperLogLog可以实现去重计数问题。
pfadd添加元素(增加对该元素的计数);
pfcount统计元素的计数。
pfmerge用于合并多个pf计数元素为同一个元素,合并pf计数值。
pf指的是HyperLogLog的发明人Philippe Flajolet教授。
HyperLogLog数据结构在计数较小时采用稀疏矩阵存储,在计数超过阈值时,转变为稠密矩阵。
HyperLogLog占据12KB存储空间,在数据量很大时,比set小了太多。
HyperLogLog的原理是调整低位连续零位的最大长度K,若K越大,概率越低,则说明计数N越大,由此通过有限的连续零位K来估算计数N,K与N存在线性相关性。占用12KB则是因为Redis的HyperLogLog实现采用
RedisBloom: Probabilistic Data Structures forRedis
The RedisBloom module provides four data structures: a scalableBloom filter, a cuckoo filter, acount-min sketch, and a top-k. Thesedata structures trade perfect accuracy for extreme memory efficiency, sothey're especially useful for big data and streaming applications.
bf.add添加元素;
bf.exists检查元素是否存在。
bf.madd添加多个元素;
bf.mexists检查多个元素是否存在。(返回分别表示每个元素存在性的0/1)
bf.reverse在添加元素之前预设布隆过滤器的key,error_rate和initial_size。
布隆过滤器:
对hash函数数量k,布隆过滤器bit数量m,预计元素数量n,错误率f,有公式:\[k = \ln2 \times (m/n) = 0.7 \times (m / n) \\f = 2^{-k} = 0.6185^{m/n}\]
以zset的score范围来划定滑动窗口。score存储timestamp,这样就可以计算得出时间窗口内的元素数量,判断访问计数是否超限。
zset不适合数量很大的限流,例如:60秒内限流100万次,100万个元素的zset会占用过大的空间。
A Redis module that provides rate limiting in Redis as a singlecommand. Implements the fairly sophisticated
genericcell rate algorithm (GCRA) which provides a rolling time window anddoesn't depend on a background drip process.
漏斗(funnel)容量有限,不满时可以装入液体,漏斗满时无法装入液体,需要等漏斗内的液体慢慢流走一部分,才能继续装入。
1 | CL.THROTTLE user123 15 30 60 1 |
通过GeoHash功能,可以快速找出指定经纬度周围的元素。
GeoHash将二维平面处理成网格,然后不断地行、列二分,对二维坐标进行编码,映射为一维整数。
Redis中,GeoHash将经纬度编码为52位整数,存入zset中,score是经纬度编码整数(zset的浮点数score可以无损存储52位整数),value是元素值。在zset中,借助skiplist来找出元素附近范围的其他元素是很容易的事情。使用坐标时,将编码整数解码还原为坐标即可。
geoadd添加经纬度坐标;
geodist计算元素之间的距离;
geopos读取元素的坐标;
geohash读取元素的经纬度编码字符串(base32编码的坐标值)。
georadiusbymember查询指定元素附近的其他元素。
注意:集群中,单个key下存储的坐标数量不宜过多(超过1MB),避免集群迁移出现卡顿。或者干脆采用独立实例,不做集群。
keys列出符合pattern的key,采用遍历算法,时间复杂度O(N)。
scan从指定cursor开始,匹配pattern,扫描count个槽位。相较于keys,scan可以避免每次遍历整个redis内存槽。
Redis本身就相当于是一个很大的HashMap。scan的遍历顺序采用高位进位加法,以此避免字典扩容和缩容时重复或遗漏遍历槽位。
zscan遍历zset元素;
sscan遍历set元素;
hscan遍历hash元素。
在业务开发中,避免大key的产生。
大key数据不论是在集群迁移时,还是在容器需要扩容时,哪怕是在回收时(因较大内存空间的分配和回收),都容易造成卡顿。
可以采用--bigkeys
选项来检索大key。
1 | redis-cli --bigkeys |
Redis是单线程程序。
Redis通过非阻塞I/O多路复用技术来提高单线程I/O处理效率。
对于每一个客户端socket连接,Redis为其关联:
对于定时任务,Redis采用最小堆进行管理:
RESP (Redis SerializationProtocol)是Redis采用地通信协议,这是一种文本协议,实现简单,解析性能好。
RESP把数据分为5种最小单元类型,制定规则:
+
开头;$<len>
开头;:
开头;-
开头;*<len>
开头。Redis采用fork机制创建子进程来导出快照。
内存空间采用COW机制,因此,父进程照常处理事务,修改的数据会记录在新的空间中,而子进程看到的仍然是fork时的内存数据,不用担心导出时数据又被更新的情况。
AOF日志记录Redis实例创建以来所有的修改性指令序列。
Redis收到客户端修改指令后,进行检查和处理,如果指令执行成功,则立即将该指令文本存储到AOF日志中。
AOF重写:长时间修改会积累大量的AOF日志,Redis可以开辟一个子进程遍历生成新的AOF指令日志,替代旧的AOF日志,起到日志瘦身的效果。(对同一个key频繁修改,会产生大量AOF日志,但实际上存一项就可以了。)
fsync:Redis定期调用fsync确保AOF日志实实在在写入磁盘,避免突然断电造成内存缓冲数据丢失。
混合持久化:快照 +AOF日志(增量)。提高重启效率,避免重做全部的AOF日志操作。
Redis客户端重排指令。将读指令连续归在一起,写指令连续归在一起。这样客户端只需要向操作系统网络写缓冲区写一次,读缓冲区读一次即可,服务器端同理。节省了网络读写的次数。
Redis可以实现begin, commit和rollback的事务功能。
PubSub, Publisher Subscriber.
消息多播,一个Publisher可以向多个Subscriber提供消息。
Subscriber需要先订阅若干个channel,随后,Publisher向channel中发布数据,Redis会将数据提供给订阅该channel的所有Subscriber。
但是,如果subscriber掉线了,过后再上线,就不会再收到掉线时错过的消息了。Redis宕机时,就相当于时没有任何subscriber的情况,会造成所有的消息都被直接丢失的情况。
Redis在5.0版本开始提供新的Stream数据结构,实现了持久化的消息队列。
32bit编译的Redis比64bit编译的版本节省一半的指针内存消耗。如使用内存不超过4GB,采用32bit即可。
相较于传统的链表,每个entry作为一个节点,都需要配备prev/next两个指针,ziplist则将多个entry以数组的形式存为一个节点,减少所需的指针空间。
每个ziplits节点存储:
inset数据结构包含:
若整数用uint16表示即可,intset就用uint16;需要升级到uint32或uint64时再动态升级。
删除key时,内存不会立即全部回收释放交给操作系统,而是会预留部分内存给未来的使用需求。
Redis有多种内存分配算法:
Redis默认使用jemalloc,该库性能稍好。
通过info memory
可以查到当前使用的内存分配库。
多个Redis节点组成Redis集群。
CAP定理指的是分布式系统的一致性(Consistency)、可用性(Availability)和分区容忍性(Partitiontolerance)不能三者兼得,最多只能满足两项。
当网络异常时,分布式节点之间无法连接,形成网络分区现象,如果要容忍分区情况,此时有两种选择:
也就是说,网络分区发生时,一致性和可用性无法两全。
Redis的主从节点之间异步同步,不能保证严格的强一致性,因此Redis的选择是放弃一致性,转而满足可用性和分区容忍性。
Redis提供的是最终一致性(Eventuallyconsistent),网络断开时,主从节点之间会出现不一致,但网络恢复后,会多策略地尽快同步,最终主从节点保持一致。
主从同步(master-slavesync):主节点与从节点之间同步,主节点把数据复制(replicate)到从节点。
从从同步(slave-slave sync):从节点把数据复制到另一个从节点。
通过引入从从同步,可以降低主节点的同步负担。
Redis同步指令流。
Redis主节点把写指令记录在本地的指令缓存(buffer)中,异步地将缓存中地指令同步到从节点,即增量同步。
指令缓存采用的是定长环形数组,因此,如果数组写满了,就会重新从头写入,也就覆盖掉了原有内容。如果网络分区发生时,有节点上产生大量写指令,为了避免指令缓存被覆盖导致写入记录丢失,不能只依赖指令缓存来保存未同步的指令。
快照同步:执行bgsave操作,把内存中的数据全部快照存储到硬盘文件中。
增加从节点:增加新的从节点时,通过快照同步为从节点全量加载数据,随后再做增量同步。
快照同步死循环问题:当快照同步太慢,或者指令缓存太小时,就会出现快照同步还没结束,指令缓存就写满的情况。这样一来,指令就不得不直接写入,那快照就过期了,又得重新做一遍快照,而重新做快照可能又太慢,指令缓存又写满了……。为避免死循环,需要设置一个合适的指令缓存大小。
快照同步需要写入磁盘,有不小的文件IO代价。而且Redis执行AOF时需要做fsync,如果此时快照同步,就不得不延后fsync,这样AOF就延后了,指令执行就延后了。
为此,Redis2.8.18开始支持无盘复制,主节点可以通过socket通信直接把快照发给从节点,避免磁盘上的文件IO代价。
Redis的复制本身时异步执行的,因此不具备强一致性。
通过wait指令,可以实现Redis的同步复制,保证系统的(在没有网络分区情况下的)强一致性。
wait可以有限等待,也可以无限等待N个从节点同步完成,再执行后续指令。
如果无限等待时,Redis出现网络分区,那么同步无法完成,就会一直阻塞,导致Redis失去可用性。
RedisSentinel集群通常包含3~5个Sentinel节点,保证Sentinel的可用性。
Sentinel集群持续监控主节点和从节点的状态,一旦出现问题,就自动提升一个可用的从节点为主节点,取代故障的不可用的主节点。
Sentinel的具体工作流程:
Codis是Redis集群方案之一,在Codis基础之上,开发出了TiDB。
单个Redis节点如果存储太多数据,会使得快照文件rdb特别大,导致同步起来很耗时,而且全量恢复也变得很慢。
Codis通过把数据分散到众多Redis节点上,来避免每个节点的数据量过大。
Codis对key做哈希,映射到1024个槽位(slots),以此求模,取得数据应该映射到的节点序号。分配完成后,Codis节点会存储槽位与Redis节点的映射关系。
Codis的扩容:可以通过增加Redis节点来扩容集群的容量。
Codis通过mget指令可以从分散的节点上取数据并汇总给用户。
Redis Cluster是去中心化的集群方案,每个节点都是对等的。
Redis Cluster把数据分为16384个槽位(
节点迁移:迁移的最小单位是槽位,流程是从源节点获取内容,然后存到目标节点,最后从源节点中删除内容。
容错:RedisCluster可以为每个主节点设置若干从节点,自动实现故障时从节点提升为主节点。
可能下线与确定下线:集群节点采用Gossip协议来广播自己的状态。一个节点发现某个节点失联,则进入可能下线(PFail,PossiblyFail)状态。集群中大多数节点都收到该节点失联的消息,则标记该节点为确定下线(Fail)状态。
Redis Stream是Redis5.0中退出的一款新的支持多播的可持久化消息队列,极大地借鉴了Kafka的设计。
RedisStream通过消息链表将所有加入的消息串起来,每个消息包含唯一ID(timestampInMillis-sequence)和消息内容(形如hash结构的键值对)。
消费组:每个Stream可以挂载多个消费组(ConsumerGroup),不同消费组互相独立,互不影响,每个消费组都有一个游标last_delivered_id在Stream数组上向前移动,表示当前已经消费到哪条消息了。
消费者:每个消费组中可以包含多个消费者(Consumer),消费者之间为竞争关系,任意一个消费者读取消息都会使消费组的游标last_delivered_id向前移动。
PEL:每个消费者有一个的PEL(Pending EntriesList),PEL是一个状态列表pending_ids,记录已经被客户端读取,但尚未收到ACK的消息ID。通过PEL可以确保客户端至少消费了消息1次,而不会在网络传输中途丢失了消息。客户端重连时,可以根据PEL重新获取一遍接收失败的消息。
分区:Redis没有原生支持分区,分区Stream可以通过在客户端设计哈希策略来实现。Kafka原生支持Partition也是通过客户端的HashStrategy来决定将不同的消息加入不同的分区的。
xgroup create:创建消费组,创建时需要指定从哪个消息ID开始消费。
xadd:加入消息;
xdel:删除消息,只设置标志位,不实际删除消息;
xrange:获取消息列表,自动过滤标记为删除的消息;
xlen:获取消息长度;
del:删除整个消息列表的所有消息。
info指令可查询:
查询方式如:
Redis内:
1 | > info memory |
Redis外:
1 | redis-cli info memory |
Sentinel集群中,主节点挂掉后,从节点取而代之,但主节点的分布式锁没有同步到从节点,新升任主节点的从节点中没有这个分布式锁,就会造成不安全性。
对多个对等的Redis实例,Redlock基于“大多数机制”,加锁时,向过半的节点发送set指令,过半的节点加锁成功,则本次加锁成功;解锁时,向所有节点发送del指令。因为Redlock需要向多个节点进行读写,考虑出错重试、时钟漂移等问题,相对单实例Redis的性能会下降一点。
设置了expire时间的key放在一个独立的字典里。
Redis的过期策略既有定期扫描,也有惰性策略。
定期扫描,Redis默认每秒10次过期扫描,扫描算法为:
为避免循环过度造成线程卡死,默认设置扫描时间上限为25ms。这个25ms的依据是,1秒10次,每次25ms,总共最多占用250ms,即1/4的CPU时间。Redis实际上限制的是CPU时间,避免过期扫描耗费超过1/4的CPU时间。
如果大量key同时过期,Redis就会循环扫描字典,删除key,直到过期字典中的过期key比例变低。当过期的key数量很多的时候,扫描时间是完全可能撞到25ms的上限的。再加上内存回收的代价,就会产生比较多的CPU消耗。如果此时新来的请求设置的超时时间很短,例如10ms,就会导致刚设置数据,就开始扫描,等25ms扫描完,才来得及处理客户端的读取操作时,key早就过期了。客户端就发现自己刚设置的值,立即去修改就会超时过期,实际上是因为Redis的过期策略在间隔中消耗了时间。
为了避免以上问题,一方面,考虑到过期策略扫描耗时,过期时间不宜设置的过短;另一方面,避免大量key同时过期,哪怕对统一的过期时间加上一个随机量也好。
惰性策略:访问key时对key的过期时间进行检查,如果过期了就删除。
从节点不会主动执行过期策略,主节点删除节点并同步del给从节点,从节点收到后写入AOF,跟着主节点照做就是。不过因为同步是异步的,所以主从节点之间强一致性无法保证。
Redis不允许发生swap,因为会造成性能急剧下降。
当Redis实际内存超过maxmemory时,有几种maxmemory-policy:
del直接删除,通常非常快,但对象非常大时,删除操作会造成单线程卡顿。
Redis4.0引入的unlink可以解决卡顿问题,unlink卸下待删除对象,然后交给后台线程去异步地回收内存。
Redis4.0为flushdb和flushall都引入了异步化,加上async选项即可,如:flushall async
。
异步删除借助异步队列实现,MainThread通过submitTask将待删除对象放入ConcurrentQueue,懒惰删除线程LazyFreeThread从中fetchTask并执行异步删除。
Redis的AOFSync需要将AOF日志同步到磁盘,需要调用sync函数,因为sync比较耗时,因此采用异步线程去调用,该异步线程也有自己的任务队列,存放AOFSync任务。
Redis在del和flush以外,也会在key过期、LRU淘汰、rename指令执行时回收内存。节点接受全量同步rdb文件后也会清空内存以载入数据。这些删除场景涉及额外的选项:
Jedis是Java的Redis开源客户端。
因为Jedis对象不是线程安全的,所以使用Jedis是从Jedis连接池JedisPool中取出一个Jedis对象归该线程独占,用完了再还给连接池。
Jedis默认没有重试机制,网络抖动造成连接断开,再发送指令就会报错。需要手动捕获JedisConnectionException进行重连处理。
rename-command既可以将已有命令更名,也可以更名为空字符串,从而屏蔽该命令被调用。
bind指令规定监听的IP地址。
requirepass设置密码访问限制,从节点masterauth设置于主节点同步连接密码。
避免UGC的Lua脚本。
避免以root权限启动Redis。
使用SSH保护Redis连接。
使用官方推荐的spiped工具对SSH通道进行二次加密。spiped是一款加密代理软件。
]]>传统单机事务模型难以应对分布式事务的处理需求,需要分布式系统。分布式系统的节点分布在网络中,难以像传统的集中式事务处理系统那样实现严格的ACID特性。
2000年7月,加州大学伯克利分校Eric Brewer教授在ACM PODC (Principles ofDistributed Computing)会议上提出了CAP猜想。
2年后,麻省理工学院的Seth Gilbert和NancyLynch从理论上证明了CAP猜想的可行性,从此CAP定理成为分布式计算领域的公认定理。
CAP定理:一个分布式系统不可能同时满足一致性(Consistency)、可用性(Availiability)与分区容错性(Partitiontorlence)这三个基本需求,最多智能同时满足其中两项。
一致性指的是多副本之间的一致性。分布式系统场景下,一个副本更新后,其他副本如果没有及时更新,那从其他副本上读取到的数据仍然是老数据,即,副本之间的数据出现不一致。
所有节点在同一时间具有相同的数据。
可用性指的是系统提供的服务必须一直处于可用状态,即,对用户请求总是在有限的时间内返回结果。
每个请求不关成功或是失败都有响应。
分区容错性指的是分布式系统遇到任何网络分区故障时,仍然能够对外提供满足一致性和可用性的服务,除非整个网络环境都发生了故障。
系统中任意信息的丢失或失败不影响系统的继续运作。
注:
根据CAP定理,分布式系统在应用中必须作出取舍,只能满足最多两个性质,意味着必须选择放弃一个性质。
放弃性质 | 说明 | 应用 |
---|---|---|
CA:放弃分区容错性(-P) | 单点集群系统,放弃分区容错性意味着放弃系统的可扩展性。实现分区容错性,简单的方法是将所有的数据(至少是事务相关的数据)放在一个分布式节点上,这样网络分区问题时,每个子网络都有依赖数据的可用副本。 | RDBMS |
CP:放弃可用性(-A) | 一旦分布式系统遭遇网络分区或其他故障,受影响的服务需要等待一定时间才能恢复对外服务,在这段时间内不可用。满足一致性,分区容忍性的系统,通常性能不是特别高。 | MongoDB, HBase, Redis |
AP:放弃一致性(-C) | 放弃分布式系统的强一致性,保证分布式系统的最终一致性。引入时间窗口的概念,隔一段时间在不同节点之间复制数据副本。 | CouchDB, Cassandra, DynamoDB, Riak |
具体地,
BASE名字取自缩写:
BASE理论由eBay架构师Dan Prichett在文章BASE: An AcidAlternative中首次提出,是对CAP中一致性和可用性权衡的结果。
BASE理论的核心思想是:即使无法做到强一致性(Strongconsistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventualconsistency)。
牺牲强一致性来获得可用性。
分布式系统在出现不可预知故障时,允许损失部分可用性。
如:
允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。
系统中所有的数据副本,经过一段时间同步后,最终能达到一个一致的状态。
不需要实时一致,达到一致所需的时间延迟,取决于网络延迟、系统负载和数据复制方案设计等因素。
实际工程实践中,最终一致性存在五类变种:
java.nio.channels.Selector
,是Windows和Linux平台的Selector.open()
所构造的Selector的底层实现完全不一样。Selector是JavaNIO中核心的多路复用选择器。线程可以将SocketChannel与选择键注册到Selector上,而Selector会选出I/O状态符合选择键条件的SocketChannel实例。
线程将SocketChannel实例与选择键注册到Selector上:
1 | try { |
Selector可以取出I/O状态符合选择键的SocketChannel集合,遍历处理:
1 | try { |
外部通过Selector.open()
方法就可以
1 | import java.nio.channels.Selector; |
在Selector
抽象类中实现为:
1 | public abstract class Selector implements Closeable { |
实际上只是一层抽象,具体调用了SelectorProvider
来提供和打开Selector
实例。
多路复用的核心功能,选出可进行I/O的通道们的键集。
1 | public abstract class Selector implements Closeable { |
该方法在Selector抽象类定义,但具体实现位于作为其子类的SelectorImpl
实现类中。
1 | /** |
SelectorImpl的select
方法调用了lockAndDoSelect
方法。传入的参数表示不执行任何操作,且默认持续等待。
在lockAndDoSelect
方法中,用synchronized关键字保护当前Selector对象,实现并发同步。内部也通过isSelect标记来防止并发select操作,实际执行的方法是doSelect
方法,该方法在SelectorImpl类中被定义,但没有实现。具体实现取决于其子类,即实现层的实现。
SelectorPrivider是一个抽象类。
1 | public abstract class SelectorProvider { |
这个provider()
方法是一个synchronized同步锁保护的单例模式,返回SelectorProvider类型的实例。
具体地,当没有实例时,需要创建实例。
创建实例通过AccessController来执行特权行为。
根据官方文档,AccessController被用于控制操作的权限和决策。
The AccessController class is used for access control operations anddecisions. More specifically, the AccessController class is used forthree purposes:
- to decide whether an access to a critical system resource is to beallowed or denied, based on the security policy currently ineffect,
- to mark code as being "privileged", thus affecting subsequent accessdeterminations, and
- to obtain a "snapshot" of the current calling context soaccess-control decisions from a different context can be made withrespect to the saved context.
这个AccessController起到三种作用:
具体地,在此处,AccessController.doPrivileged(...)
方法起到的是第二个作用,授予权限,执行特权代码:
Performs the specified PrivilegedAction with privileges enabled.
The action is performed with all of the permissions possessed by thecaller's protection domain.
该方法的输入参数是一个实现了PrivilegedAction
接口的匿名类,该匿名类实现了接口的run()
方法。该方法依靠外层提供的特权权限,来实例化一个SelectorProvider
。实例化的过程分三种优先级:
loadProviderFromProperty()
loadProviderAsService()
provider = sun.nio.ch.DefaultSelectorProvider.create()
loadProviderFromProperty
第一优先级通过系统检查java.nio.channels.spi.SelectorProvider
是否存在,如果存在则加载,反之,则返回false
。
Service-provider classes for the java.nio.channels package.
java.nio.channels.spi
包提供了一批ServiceProvider的类。
1 | public abstract class SelectorProvider { |
该方法首先读取检查系统属性中,键java.nio.channels.spi.SelectorProvider
是否有设置值。如果没有,则返回false,如果有,用这个值加载SelectorProvider类。
该方法通过Class.forName
方法,指定通过系统类加载器在运行时动态加载系统属性中设置的SelectorProvider类(如指定)。
loadProviderAsService
如果第一优先级所需的java.nio.channels.spi.SelectorProvider
不存在,则需要启动第二优先级的加载工作。
如果META-INF/services
中,存放了java.nio.channels.spi.SelectorProvider
的jar文件,则通过系统类加载器加载该服务。
1 | public abstract class SelectorProvider { |
该方法通过ServiceLoader
来加载服务,选取可见的第一个SelectorProvider
实例。
sun.nio.ch.DefaultSelectorProvider
如果上述SelectorProvider都不存在,就会加载sun.nio.ch.DefaultSelectorProvider
作为最终选择。
实际运行中,如果没有实现和配置前两种,默认会启用该最终优先级。
DefaultSelectorProvider的对外提供统一的接口,内部仅仅是完成对实现类的实例化,而具体实例化什么类,取决于JDK的操作系统版本。
具体地,该sun.nio.ch.DefaultSelectorProvider
对外提供一致接口,其create
方法实际上仅仅是一层封装,只是实现了一个new
实例化操作,但不同操作系统平台的JDK的内部实现不同:
sun.nio.ch.WindowsSelectorProvider
类。sun.nio.ch.EPollSelectorProvider
类。Windows JDK11:
1 | /** |
Linux JDK11:
1 | /** |
这一层都继承自SelectorProviderImpl抽象类,实际上也没有实现什么特别的功能逻辑,只是调用对应的SelectorImpl实现类。
这一层实现了从SelectorProvider到SelectorImpl的交互。
具体到每种SelectorImpl是如何实现的,在下一节实现层具体分析。
1 | /* |
1 | public class EPollSelectorProvider |
abstract class Selector
abstract class AbstractSelector extends Selector
abstract class SelectorImpl exntends AbstractSelector
class WindowsSelectorImpl extends SelectorImpl
class EPollSelectorImpl extends SelectorImpl
abstract class SelectorProvider
abstract class SelectorProviderImpl extends SelectorProvider
class WindowsSelectorProvider extends SelectorProviderImpl
class EPollSelectorProvider extends SelectorProviderImpl
class DefaultSelectorProvider
在WindowsJDK11中,其实例化的是sun.nio.ch.WindowsSelectorProvider
类返回给上层使用。
简单回顾一下,WindowsSelectorProvider实现了从对外的SelectorProvider到具体的WindowsSelectorImpl实现类的转接。
该类继承自SelectorProviderImpl
抽象类,是对其的具体实现,供外部抽象层调用,实现的只是转接调用,调用WindowsSelectorImpl
这一个实现类。
1 | public class WindowsSelectorProvider extends SelectorProviderImpl { |
概括地来讲,WindowsSelectorImpl的底层实现是通过JNI接口调用地本地poll方法,但是不是简单调用,而是进行了多线程的改进。
为什么要采用多线程呢?因为poll方法本身可以处理的文件描述符(filedescriptor)数量是有限的,一般和select方法类似,不超过1024个。实际的应用场景中,需要并发处理的文件描述符是完全有可能超过这个上限的。WindowsJDK11中的实现则采用多线程对poll进行改进,一个线程能处理的文件描述符数量是有限的,那么如果文件描述符数量很多,用多个线程分摊处理不就好了么。
类型 | 变量 | 说明 |
---|---|---|
SelectionKeyImpl[] | channelArray | The list of SelectableChannels serviced by this Selector. Every modMAX_SELECTABLE_FDS entry is bogus, to align this array with the pollarray, where the corresponding entry is occupied by thewakeupSocket |
PollArrayWrapper | pollWrapper | The global native poll array holds file decriptors and eventmasks |
List<SelectThread> | threads | A list of helper threads for select. |
Pipe | wakeupPipe | Pipe used as a wakeup object. |
FdMap | fdMap | Maps file descriptors to their indices in pollArray |
SubSelector | subSelector | SubSelector for the main thread |
Object | interruptLock | Lock for interrupt triggering and clearing |
Object | updateLock | pending new registrations/updates, queued by implRegister andsetEventOps |
Deque<SelectionKeyImpl> | newKeys | |
Deque<SelectionKeyImpl> | updateKeys |
Windows平台JDK11是如何select出对应状态的SocketChannel的呢?
抽象层的Selector.select()
调用由SelectorImpl.select()
实现,而该实现主要是调用了SelectorImpl.lockAndDoSelect()
,其中调用SelectorImpl.doSelect()
,该方法在Windows平台的JDK11中由WindowsSelectorImpl.doSelect()
具体实现。
1 | /** |
调用WindowsSelectorImpl.doSelect()
方法,执行的流程主要为:
subSelector.poll()
,这是主线程自己调用自己的subSelector在执行poll操作。threads.size()>0
的情况,那么就需要通过finishLock.waitForHelperThreads()
的同步操作来等待辅助线程们完成他们的工作。doSelect
操作更新过的键的数量。辅助线程是WindowsSelectorImpl.SelectThread
类的实例,线程类最核心的内容就是其实现的run
方法。
1 | // Represents a helper thread used for select. |
不难发现,辅助线程的线程类的实现中,其执行的核心操作其实就是调用了subSelector.poll(index)
,以此对本线程负责的文件描述符进行poll操作。
那这个subSelector
又是怎么做的呢?
前面介绍了主线程和辅助线程,两者都有一个subSelector实例,他们在执行poll操作的时候都是调用的subSelector.poll()
。
1 | private final class SubSelector { |
查看源码可知,SubSelector的poll()
和poll(index)
方法实际上都是对poll0()
方法的一层适配封装,实际上调用的就是poll0()
。
从上面的源码可以看到,poll0
方法并不是在Java中实现的,而是通过JNI调用的本地实现。
在LinuxJDK11中,其实例化的是sun.nio.ch.EPollSelectorProvider
类返回给上层使用。
类似的,LinuxJDK11是通过EPollSelectorProvider提供外部访问接口的。
1 | public class EPollSelectorProvider |
该openSelector
方法主要是通过EPollSelectorImpl实现类来实例化一个EPollSelector并返回。
Linux平台JDK11是如何select出对应状态的SocketChannel的呢?
实质上是调用的EPoll.wait
方法来返回已经就绪的文件描述符数量。
1 | /** |
具体地,在EPollSelectorImpl.doSelect
方法中,和WindowsSelectorImpl中的实现类似:
EPoll.wait
方法来获取处于就绪状态的I/O文件描述符数量;doSelect
更新过的键的数量。EPoll作为Linux内核提供的多路复用器,JDK11选择通过JNI接口来调用其功能。
JDK11中EPoll
类是一个简易的包装类,epoll的实现不由JDK负责。
1 | /** |
1 | static native int wait(int epfd, long pollAddress, int numfds, int timeout) |
该方法调用的应该是Linux中的epoll_wait
系统调用。根据man epoll_wait
查阅的Linux手册,具体说明:
The epoll_wait() system call waits for events on the epoll(7)instance referred to by the file descriptor epfd. The memory areapointed to by events will contain the events that will be available forthe caller. Up to maxevents are returned by epoll_wait(). The maxeventsargument must be greater than zero.
The timeout argument specifies the number of milliseconds thatepoll_wait() will block.
也就是说,JDK调用的EPoll.wait
方法会在timeout时间内阻塞等待epoll的文件描述符epfd所引用的事件发生,发生后,其返回的结果是代表事件数量的整数。
从表层的Selector
查到底层的WindowsSelectorImpl
与EPollImpl
,经过一层层抽丝剥茧,可以看到JDK在设计上清晰地体现着将抽象与实现分离的“依赖倒置原则”——顶层调用不应该依赖于底层实现,底层实现也不应该针对于顶层调用,双方都应该依赖于抽象。
考虑到Linux内核已经提供了好用的epoll多路复用,足以处理大规模的并发连接,JDK11通过JNI接口对epoll相关的系统调用进行本地调用即可,其实现也显得相对简单。Windows并未提供Epoll这样的多路复用模型,为解决poll存在的并发连接数量有限的问题,JDK11通过分而治之的分治思想,拉辅助线程来分担任务,通过实现动态多线程poll巧妙地实现了处理大量并发连接的能力。
最终,无论是Windows还是Linux,要想研究多路复用机制的更深层的实现原理,还是需要研究操作系统层级的实现原理。
]]>ab - Apache HTTP server benchmarking tool
ab
is a tool for benchmarking your Apache HypertextTransfer Protocol (HTTP) server. It is designed to give you animpression of how your current Apache installation performs. Thisespecially shows you how many requests per second your Apacheinstallation is capable of serving.
Apachebenchmark是一款Apache提供的HTTP服务器压力测试工具,随Apache安装。
1 | sudo apt install apache2-utils |
我测试了我的
在我的实验室台式机上进行了测试:
1 | jyshen@JYSHEN-WORKPC:~$ ab -n 100000 -c 1000 http://localhost:8080/ |
另外,我在实验室的新服务器上进行了测试:
1 | (base) sjy@h3c-UniServer-R5200-G3:~$ ab -n 100000 -c 1000 http://localhost:8080/ |
我还在另一台计算服务器上进行了测试:
1 | jyshen@ubuntu:~$ ab -c 1000 -n 100000 http://127.0.0.1:8080/ |
GitHub地址:
Webbench是RadimKolar在1997年写的一个在linux下使用的非常简单的网站压测工具。它使用fork()模拟多个客户端同时访问我们设定的URL,测试网站在压力下工作的性能,最多可以模拟3万个并发连接去测试网站的负载能力。官网地址:http://home.tiscali.cz/~cz210552/webbench.html
1 | # prerequisite |
运行1万个并发client,1秒钟。
在我的实验室台式机上测试:
1 | jyshen@JYSHEN-WORKPC:~$ webbench -c 10000 -t 1 http://localhost:8080/ |
实验室的服务器无法fork创建出相同数量的1万子进程,提示:
1 | (base) sjy@h3c-UniServer-R5200-G3:~/HearyHTTPd/webbench-1.5$ webbench -c 10000 -t 1 http://localhost:8080/ |
通过htop
查了下,服务器上还有其他同学的不少计算程序在进行。暂时无法对比。
JDK对外提供Executors
类的三个静态方法供调用,可以快速生成线程池:
1 | public static ExecutorService newSingleThreadExecutor() |
退化为只包含一个线程的“线程池”。
1 | public static ExecutorService newFixedThreadPool (int nThreads) |
包含固定数量线程的线程池。
1 | public static ExecutorService newCachedThreadPool() |
按需创建线程的线程池。
实质上,以上三个对外的静态方法,本质上都实例化了同一个类型,即:ThreadPoolExecutor
,该类继承自抽象类java.util.concurrent.AbstractExecutorService
,该抽象类实现了ExecutorService
接口,该接口又继承自Executor
接口。其中,ExecutorService
就是一般外部调用线程池实例的抽象接口。
ThreadPoolExecutor
提供构造函数:
1 | /** |
可以设置线程池的一系列参数:
1 | /** |
newSingleThreadExecutor
具体设置参数为:
LinkedBlockingQueue
的实例中。外面套了一层FinalizableDelegatedExecutorService
实际上是该Executors
类定义的一个内部静态类:
1 | private static class FinalizableDelegatedExecutorService |
finalize
方法,负责关闭线程池。该类进一步继承自另一个内部静态类DelegatedExecutorService
,这是一个包装类,用于控制对外的提供的方法:
1 | /** |
1 | /** |
newFixedThreadPool
具体设置参数为:
nThreads
,这保证了线程池中有且只有nThreads
个线程。nThreads
个线程,所以无所谓超时终止,因此保活时间为0。LinkedBlockingQueue
的实例中。除了可以设置多个线程,其他参数与上一个单线程的线程池非常相似。
1 | /** |
newCachedThreadPool
具体设置参数为:
SynchronousQueue
的实例中。外部使用线程池时,调用的是submit
方法。该方法在接口ExecutorService
中定义,在抽象类AbstractExecutorService
中实现。
具体地,在抽象类AbstractExecutorService
中,submit
的实现为:
1 | /** |
其中,FutureTask
负责统一把输入的无论是Runnable
还是Callable
都统一转换为FutureTask
实例,并以RunnableFuture
接口的抽象形式返回。
随后,调用execute(ftask)
方法来执行新提交的任务。该方法在抽象类的子类——ThreadPoolExecutor
中具体实现。
任务提交后需要执行起来,submit
中执行的方法execute(ftask)
在抽象类的子类——ThreadPoolExecutor.execute
中具体实现:
1 | /** |
可以看到,当工作线程较少,还不到核心线程数时,该方法会添加一个新线程,并把输入的Runnable command
交给新线程执行。
如果已经达到核心线程数,该方法进行了一系列谨慎的检查工作,并且把输入的Runnable command
加入到了workQueue
中,具体地:
检查线程池控制字判断线程池是否在工作,如果是,就把任务加入工作队列workQueue.offer(command)
:
LinkedBlockingQueue
就属于这一类,这种情况下,线程数量不会超过corePoolSize
的核心线程数。reject
(reject
方法会进一步调用RejectedExecutionHandler
实例的handler.rejectedExecution(command, this);
方法以便处理这种任务被线程池拒绝的情况);SynchronousQueue
就属于这一类,如果没有线程阻塞在读取上,就无法插入新的任务,即会返回false。这么一来,就会启动新的线程,毕竟有阻塞在读取上的线程,才能加入新的任务。这意味着线程数量完全有可能超过corePoolSize
规定的核心线程数。工作队列是一个阻塞队列,用于解决生产者-消费者问题。也就是说,execute
扮演的是一个生产者的角色,它负责把检查过的任务加入到工作队列中,供线程池中的工作线程取出并执行。
在ThreadPoolExecutor
中,工作队列workQueue
是一个BlockingQueue<Runnable>
:
1 | /** |
在上述execute
方法中,要执行Runnable
任务,需要线程池中有工作线程,是通过调用addWorker
实现的。
具体地,线程池工作线程的创建和添加操作在ThreadPoolExecutor.addWorker
方法中具体实现:
1 | /** |
其中,会根据输入参数boolean core
来约束工作线程数的上限。如果core==true
,则线程数不超过corePoolSize
;否则线程数上限为maxPoolSize
。
线程池中保有的工作线程作为消费者的一方,要从工作队列中取出任务并执行。
具体地,线程池工作线程的主循环在ThreadPoolExecutor.runWorker
方法中具体实现:
1 | /** |
开头task
初始值为firstTask
,是线程池的成员变量,用于引用初始任务,通常为空。因此,线程池的主线程作为workQueue
的消费者,通常情况下都是通过getTask()
方法来取出任务。
取出任务后,谨慎地进行线程池的状态检查,并在运行任务的前后,分别调用beforeExecute
和afterExecute
方法。这两个方法在ThreadPoolExecutor
中实现内容为空,也就是说不做任何事情。这两个方法是预留的,可以被继承实现,以增加额外的检查和功能(如:记录日志)。
运行任务显得很简单,线程池的工作线程执行Runnable
任务实例的task,run()
方法即可。
ThreadPoolExecutor.getTask()
的实现:
1 | /** |
可以看到,根据timed
变量,决定是限时等待型读取或是阻塞型读取。
timed==true
,则在主循环中限时等待提取工作队列中的任务,即workQueue.poll
方法:null
。timed==false
,则在主循环中阻塞式提取工作队列中任务,即workQueue.take
方法:上述“3.2开始执行任务——execute”中解释了LinkedBlockingQueue与SynchronousQueue为什么会分别用于不同的ThreadPoolExecutor。尤其是同步队列,CachedThreadPool
依靠其插入失败就可以检测到没有数量匹配的读线程,由此增加线程池的线程数。
这个容器就比较特别了,虽然名字是队列,但实际上没有任何容量。
A blocking queue in which each insert operation must wait for acorresponding remove operation by another thread, and vice versa. Asynchronous queue does not have any internal capacity, not even acapacity of one. You cannot peek at a synchronous queue because anelement is only present when you try to remove it; you cannot insert anelement (using any method) unless another thread is trying to remove it;you cannot iterate as there is nothing to iterate. The head of the queueis the element that the first queued inserting thread is trying to addto the queue; if there is no such queued thread then no element isavailable for removal and poll() will return null. For purposes of otherCollection methods (for example contains), a SynchronousQueue acts as anempty collection. This queue does not permit null elements.
使用该容器时,必须先取再插。也就是说,对于一个同步队列,如果没有任何线程在读取它,别的线程就无法对其插入新数据。通常,需要先启动一个线程读取同步队列,此时同步队列尚无数据,则该读线程会处于阻塞等待地状态。随后,启动一个线程向同步队列中插入数据,此时,阻塞等待数据的读线程会唤醒并读取插入数据。
因为其插入时必须要有读线程的特性,该容器被应用于检测读线程少于插入任务数量的情况,引导线程池增加新线程。
不难理解,这是一个基于链表实现的阻塞队列。
An optionally-bounded blocking queue based on linked nodes. Thisqueue orders elements FIFO (first-in-first-out). The head of the queueis that element that has been on the queue the longest time. The tail ofthe queue is that element that has been on the queue the shortest time.New elements are inserted at the tail of the queue, and the queueretrieval operations obtain elements at the head of the queue. Linkedqueues typically have higher throughput than array-based queues but lesspredictable performance in most concurrent applications.
既然是链表实现,那一般理解是可以无界的,当然也可以指定大小限定为有界。
链表阻塞队列采用FIFO模式,队列头是最早插入的,队列尾是最新插入的,取出时依据FIFO顺序。
基于链表阻塞队列在并发应用中吞吐量通常比基于数组的阻塞队列更大,因为基于链表的阻塞队列不至于同步锁定整个数组容器。基于链表的阻塞队列实际上在读写时,锁定入队和出队的位置就可以了。
不难理解,这是一个基于数组实现的阻塞队列。
A bounded blocking queue backed by an array. This queue orderselements FIFO (first-in-first-out). The head of the queue is thatelement that has been on the queue the longest time. The tail of thequeue is that element that has been on the queue the shortest time. Newelements are inserted at the tail of the queue, and the queue retrievaloperations obtain elements at the head of the queue. This is a classic"bounded buffer", in which a fixed-sized array holds elements insertedby producers and extracted by consumers. Once created, the capacitycannot be changed. Attempts to put an element into a full queue willresult in the operation blocking; attempts to take an element from anempty queue will similarly block.
This class supports an optional fairness policy for ordering waitingproducer and consumer threads. By default, this ordering is notguaranteed. However, a queue constructed with fairness set to truegrants threads access in FIFO order. Fairness generally decreasesthroughput but reduces variability and avoids starvation.
既然是基于数组实现的,那容器肯定是有界的,创建时就确定的,无法动态变化。这样一来,如果数组中存满了,再插入新的数据就需要阻塞至有元素被取走,同样地,如果数组中没有元素,读取操作需要阻塞至有元素被插入。
另外,数组阻塞队列还存在一个公平策略,如果严格要求保障FIFO的出入队列顺序,需要启用公平策略,这样可以避免饥饿问题(因为否则的话,可能有些元素长时间都不会被轮到取出来),但是也会减少吞吐量。
]]>