路人甲's Notes

http://blog.51cto.com/guojuanjun 不再更新


  • 首页

  • 归档

vmstat中的cs列

发表于 2019-01-08

Linux将内存分为两部分:用户空间和内核空间。用户空间的程序无法访问内核空间的数据,反之依然,所以他们之间的指针无法直接使用。用户程序想访问内核空间,比如网络,文件系统的数据,则需要通过系统调用的方式进入内核空间。

在不同的空间时,CPU运行的环境是不同,然后有了上下文的概念。CPU总处于以下状态中的一种:
1)内核态,运行于进程上下文,内核代表进程运行于内核空间。
2)内核态,运行于中断上下文,内核代表硬件运行于内核空间。
3) 用户态, 运行于进程上文件的用户空间。

进程上下文

一个进程的上下文可以分为三个部分:用户级上下文、寄存器上下文以及系统级上下文:
1) 用户级上下文: 正文、数据、用户堆栈以及共享存储区;
2) 寄存器上下文: 通用寄存器、程序寄存器(IP)、处理器状态寄存器(EFLAGS)、栈指针(ESP);
3) 系统级上下文: 进程控制块task_struct、内存管理信息(mm_struct、vm_area_struct、pgd、pte)、内核栈等。

在进程调度的,系统需要将上面全部的信息进行切换,新的进程才可以运行。

#### 模式切换
我们常说的系统调用,只是当前进程上下文从用户态进入内核态,只有寄存器上下文进行了切换。相对进程切换要简单很多。通过数据观察,模式切换不在vmstat的cs列统计中。
http://blog.51cto.com/guojuanjun/1951816

中断上下文

CPU接收到硬件的中断信号, 则切换到中断上下文,进入内核态。由于中断处理程序只会运行在内核态,而且与进程无关。(中断处理程序不能睡眠,没有进程调度单元,睡了就没法唤醒了)。所以进程上下文切换到中断上下文时, 进程上下文的第一部分用户级上下文不需要切换。

那vmstat的cs是否包含中断上下文的切换呢,如图
logo
通过这个数据观察, in(中断数)明显小于cs(上下文切换数)。 通常情况下,每次中断都会产生到中断上下文的切换,in不应该小于cs。反证可知,cs列不包括到中断产生的切换。

综上所述:vmstat中的cs列只包括进程上下文的切换。

进程调度程序只能运行在内核态,所以进程调度也只会发生在内核态:
1) 用户态进程无法实现主动调度。只有通过系统调用进入内核态,在系统调用处理完,返回用户态时,是一个调度点。这个时候会检测有没有设置NEED_RESCHED标志。
2) 若用户态进程没有系统调用,则通过中断处理程序返回时进行被动调度。
3) 内核进程可直接调用schedule()进行主动调度。

KMP算法中Next数组的意义

发表于 2019-01-01 | 更新于 2019-01-02

例如主串为a1,a2,a3,a4,a5,a6,…,an,模式串为b1,b2,b3,b4,b5,b6,…,bm。不失一般性,我们从主串头部字符开始匹配,a1=b1,a2=b2,a3=b3,a4=b4,a5=b5,a6!=b6,到达a6时匹配不成功, 如图所示:
logo
按常规匹配算法,我们应该从a2开始重新匹配。假定此时可以匹配成功即a2=b1,a3=b2,a4=b3,a5=b4,…,如图所示:
logo
按照前一次匹配所得的结论:a2=b2,a3=b3,a4=b4,a5=b5,可以推得b1=b2,b2=b3,b3=b4,b4=b5。即b1b2b3b4=b2b3b4b5相等。若模式串不满足这个条件,我们认为从a2开始对比是没有必要的,因为没有匹配成功的可能。
我们再假定从a2开始匹配没有成功,我们需要从a3开始匹配,假定此时匹配成功,即a3=b1,a4=b2,a5=b3,…,如图:
logo
参照第一次匹配的结果,我们得出b1=b3,b2=b4,b3=b5即b1b2b3=b3b4b5。从模式的特征,可以帮助我们跳过那些不可能成功的匹配动作,提升算法效率。
我们总结下前面的结论:当第6个字符b6与主串匹配不成功时,我们的模式串应该右移几位去再次匹配。若模式串满足b1b2b3b4=b2b3b4b5,那我们只需移动一位。若字符串满足b1b2b3=b3b4b5,我们可以移动两位。同理可证b1b2=b4b5时,我们可以移动三位。即已完成匹配的子串b1b2b3b4b5的前缀与后缀的重合度的最大值决定了模式串移动的位数。重合度越大,移动的位数越小。模式串的移动距离决定了匹配失败的b6下应该和模式串中的哪个字符对比。而Next就是这种条件的表达方式。

Linux中Cache与Buffer的区别

发表于 2018-12-26

Linux中Free命令有一个Buffer/Cache很难理解。Buffer与Cache有什么区别? 通过dd操作,Vmstat数据输出来看,对于文件系统的读写对Cache的影响较大,而读写Raw设备对Buffer的影响较大,证明了大家认为的Cache为文件页缓存,而Buffer为IO块缓存。但是文件操作虽经过文件系统,但毕竟要从磁盘设备上读取,势必要经过块缓冲(Buffer),为什么文件操作对Buffer 的影响不明显呢? 后来找到下面这篇文章(为防止信息丢失,我COPY过来了,请多包涵)。很多Unix是这样实现的,对于文件而言,一份数据会在Buffer与Cache中各缓存一份,简单但低效。后期Linux进行了统一,数据如果是块设备中的文件,则只缓存一份在PageCache中,而Buffer中不再缓存了。然而内核磁盘读写操作的仍然是Buffer,Buffer只是简单的指向Page Cache。

附原文:
The page cache caches pages of files to optimize file I/O. The buffer cache caches disk blocks to optimize block I/O.

Prior to Linux kernel version 2.4, the two caches were distinct: Files were in the page cache, disk blocks were in the buffer cache. Given that most files are represented by a filesystem on a disk, data was represented twice, once in each of the caches. Many Unix systems follow a similar pattern.

This is simple to implement, but with an obvious inelegance and inefficiency. Starting with Linux kernel version 2.4, the contents of the two caches were unified. The VM subsystem now drives I/O and it does so out of the page cache. If cached data has both a file and a block representation—as most data does—the buffer cache will simply point into the page cache; thus only one instance of the data is cached in memory. The page cache is what you picture when you think of a disk cache: It caches file data from a disk to make subsequent I/O faster.

The buffer cache remains, however, as the kernel still needs to perform block I/O in terms of blocks, not pages. As most blocks represent file data, most of the buffer cache is represented by the page cache. But a small amount of block data isn’t file backed—metadata and raw block I/O for example—and thus is solely represented by the buffer cache

NIO真空下的技术

发表于 2018-12-24

NIO在使用过程中,有很多很多问题需要注意,甚至BUG需要规避。不是经验丰富的工程师团队,不建议从头构建NIO程序,最好采用Grizzly,Netty等开源框架。本文记录了经常遇到的几个小细节,一作备忘,二为增强理解。

1. channel.close将cancel掉channel的所有SelectionKey

AbstractSelectableChannel维护一个SelectionKey数组,存储注册所得的所有SelectionKey,一旦channel.close,这里所有的SelectionKey将被cancel掉。

2. 只有非阻塞Channel才能注册,否则会报IllegalBlockingModeException

比如FileChannel不可以改为非阻塞,所以也无法注册。

3. 一个Channel在同一个Selector上注册两次,返回同一个SelectionKey

Channel注册时,会被检查是否已在当前Selector上注册过,若是,则返回注册的SelectionKey。

4. sk.interestOps操作与register操作在同一个线程

其实这不是必须的,Grizzly,Netty,和Tomcat都是这么写的,原先本人也误以为不在同一个线程会有问题,后来证明不是这样的。之所以这么写,是规避并发性能问题。AbstractSelectableChannel中有两把锁keyLock和regLock,interestOps与register操作会导致这两把锁的激烈竞争,所以都把这些操作交给注册该Channel的Reactor线程来完成。

5. selector.selectedKeys中的SelectionKey要及早清除

selectedKeys需要及时remove,否则后面select()将不阻塞,从而导致CPU 100%。

6. 不轻易注册WRITE事件

因为可写事件来得太容易,select()将很难不阻塞,从而导致CPU 100%。

7. selectionKey.cancel的延后性

取消Key只有在下一次select操作之后才会从key set中清除。

8. select()操作之前的wakeup将导致下一次的select()不阻塞。

getOutputStream() has already been called for this response的来路

发表于 2018-12-19 | 更新于 2018-12-21

除了上篇的response has been committed,另一个常见的异常getOutputStream() has alreadybeen called for this response 或 getWriter() has already been called for this response。这类异常并非Tomcat专有的,在weblogic中是为strict servlet API: cannot call getWriter() after getOutputStream 或 strict servlet API: cannot call getOutputStream() after getWriter().

意思就是调用了getWriter之后,不能再调用getOutputStream, 反之亦然。getWriter和getOutputStream都是servlet引擎用来向响应报文中写入数据的方法。那为什么要限制同时使用呢?

getWriter()方法返回是PrintWriter对象,PrintWriter用来写入字符或字符串。getOutputStream()返回ServletOutputStream,其接口为OutputStream,所以getOutputStream()方法用来写入字节流。

getWriter和getOutputStream就像向响应报文中输入数据的管子,而且两个管子中的数据格式不一样。如果两个管子同时向响应报文中灌入数据,那数据是不是就乱了呢? 所以中间件安排他俩互斥使用。

既然PrintWriter用来写入字符或字符串,那把PrintWriter写入的字符串转换成字节流不就可以同时用了。其实PrintWriter写入数据的时候有可能响应字符集还没有确定,你无法知道应该用哪个字符集去转换。(Tomcat提供了ENFORCE_ENCODING_IN_GET_WRITER参数)。具体其他的原因,只能咨询规范的作者了。在Servlet2.5规范SRV.15.2.22节提到:Either this method or getWriter() may becalled to write the body, not both.

什么情况下出现前面提到的异常,一般在我们需要输出一种特殊格式的响应内容时会出现。比如验证码,导出文件等。大多程序员会把这些业务放在Jsp中,然后通过response.getOutputStream().write()输出数据。而Jsp页面中的可能含有html内容,即使只是一个空格或者换行。这些内容是通过out.write输出的, 这个out其实就response.getWriter()的值。这样就会在你不经意中被调用response.getWriter(),可能位于response.getOutputStream()调用之前,也可能在之后。无论如何,异常是跑不了啦。

解决这类问题的方法当前很简单,就是隐藏的response.getWriter()的调用:

  1. 使用Servlet输出验证码,导出文件。
  2. 如果确要用JSP文件,最好的办法看编译后的java文件,有没有out.write操作。有的话,检查是JSP哪一行引起的,删除即可。
    大多数情况出现这个异常,并没影响正常业务。其实异常是后一个get调用引起的,而且这个调用非常大的机率是不需要的,调用他的时候响应报文大概已经在去向客户端的路上了。

Maven使用过程中的几个细节

发表于 2018-12-06 | 更新于 2019-01-14

1. Settings.xml的生效问题

maven共有两个settings.xml:
全局配置: ${M2_HOME}/conf/settings.xml
用户配置: ${user.home}/.m2/settings.xml
maven使用两者的合并信息,用户配置的优先级高于全局配置。其有mvn -s(–settings)可以修改用户配置.

2. 多个Profile的生效问题

Profile根据环境条件被激活:
1) <activeByDefault>true</activeByDefault>
2) <activeProfile>profileName</activeProfile>
3) ..
如果多个Profile同时激活,则Profile会合并,相同的配置则后定义的Profile优先级比较高。与active的顺序无关。

3. repository的搜索顺序

repository分两类:本地仓库和远程仓库。远程仓库包括全局仓库,项目仓库和中央仓库。搜索顺序如下:
1) 本地仓库: 就是本地的缓存目录,一般~/m2/repository
2) 全局仓库: setting文件profile中配置的仓库。
3)项目仓库: 项目pom.xml中profile配置的仓库。
4)项目仓库: 项目pom.xml中配置的仓库。
5) 中央仓库:就是central仓库

什么是response has been committed

发表于 2018-12-05 | 更新于 2018-12-21

在Tomcat或其他中间件产品上,有时或突然跳出一个response has been committed的异常。网上也有很多解决这个问题的攻略。为什么很多,或许写答案的人也不知道为什么这个方法解决了这个问题,而是各种尝试之后, 终于解决了。(边何人初见月,江月何年初照人)
什么叫response has been committed? 数据库有已提交的概念,表示相关事务已完成,可能数据并没有写入数据磁盘,而是位于redo日志中,无论怎么,事务已成功,即使此时掉电,也不会丢失更新。已提交的事务还可以撤回吗?当然不可以。(更多可了解redo与undo的区别)。

而Tomcat对于Response的已提交也是一样,表示Response已成即定事实,撤回不了了。Tomcat在这个response有任意一个字符发送到网络时,会标识response已提交。为什么呢?数据已经发出去,再也无法中断/撤回他了。所谓覆水难收就是这个意思。

我们看response has been committed一般发生在什么时候:
1) Java.lang.IllegalStateException: Cannot call sendRedirect() after the response has been committed sendRedirect
什么意思呢? 当我想放弃当前的响应内容,回复一个304的响应让客户端重新请求。但这里因为发送缓冲区不足,Tomcat已将原先准备好的一些数据发送出去了,当然响应头会被首先发出。Tomcat是告诉你:哥们,不好意思,状态为200的响应我已经发送出去了。
2)Cannot forward after response has been committed
同1)一样,你想抛弃你当前的响应(response),forward到一个新的sevlet去响应当前的请求,Tomcat告诉你:你的响应已经发出去了, 撤不回来了。

3) sendError等。
sendError意欲改变响应状态码。设想这时状态码已经在网络链路间跋山涉水,奔向客户端,打个电话让他回来吗?

明白了问题产生的原因,更容易寻找解决的方法。为什么Tomcat会在我们决定sendRedirect时已经将response的数据发出去? 除了代码可能作了flush(将数据写入网络)操作,也有可能发送缓冲区较小,Tomcat只能将原有的数据写出网络。一个治标的方法,就是将缓冲区调大。治本的方法当然是:写代码要有计划。不然何至覆水难收?

更多细节参考 Servlet4 规范-5.1 Buffering

Istio流量分析

发表于 2018-12-04 | 更新于 2019-01-02

注: 文中示例信息来自istio官方book示例.

istio中的边车代理envoy会拦截到两类流量:
1) 入流量: 其他Pod访问当前Pod中业务服务的流量。入流量访问的集群以inbound开头命名:
inbound|9080|v1|ratings.default.svc.cluster.local
2) 出流量: 当前Pod中业务服务访问其他服务的流量,包括其他POD中的业务服务和istio系统相关的服务。入流量访问的集群命名以outbound开头: outbound|9080|v1|ratings.default.svc.cluster.local

入流量的上游集群比较简单,一般只有一个:

1
2
3
4
5
6
7
8
"hosts": [
{
"socket_address": {
"address": "127.0.0.1",
"port_value": 9080
}
}
]

这个Endpoint(127.0.0.1:9080)指向本地服务。出流量的上游集群包括所有的服务集群,因为当前POD的本地服务有访问任何服务的可能。

Envoy代理拦截到出流量之后,会根据被访问service的域名,URL等相关信息定位到特定的路由,然后到特定的集群,再按集群内部的负载算法获取特定的Endpoint地址,将流量发送给这个Endpoint。流量图大体如下:
logo
这个复杂的过程中,有两个重要的参数:
1) litener.use_original_dst:
如果采有iptables重定向连接, 那么envoy接受连接的监听端口与请求连接的原始目的地址是不同的。如果listener该属性设置为true,该listener将接收到的连接转交给符合请求原始地址的Listener。(透明代理获取原始地址?)由新的listener接受和处理该连接请求。前一个Listener相当于只分发连接。在istio中,这个Listener命名为命virtual。到达envoy的流量统一重定向到virtual listener中(监听地址为0.0.0.0:15001),由它来选择合适的Listener处理流量。

2) litener.bind_to_port
这个属性在api-v2中找不到了,也没找到新替代者的说明。在istio 1.0版本中仍然使用他,但标记已过期。该属性表示Listener是否绑定主机端口。那么不绑定的端口的Listener,如何接受流量?是由标识use_original_dst的Listener(上文中的virtual Listener)接受流量,并转发给他。0.0.0.0_9080和10.244.2.20_9080都是bind_to_port为false的Listener,等待virtual listener分发的流量。0.0.0.0_9080接收所有9080端口的出流量,10.244.2.20_9080接受入流量。
logo
9080本地服务的端口,15000是envoy代理的管理端口,0.0.0.0:15001就是virtual listener的监听地址,envoy用来接收流量。在evnoy的配置中看到其他listener都没有绑定端口。

Note:
获取envoy全配置的方法:
kubectl exec productpage-v1-f5fcbd489-s7tfz -c istio-proxy – curl http://127.0.0.1:15000/config_dump

CPU主频与MIPS

发表于 2018-11-26 | 更新于 2018-12-21

今天在一台龙芯CPU的机器上发现一个BogoMIPS参数,刚好位于CPU MHZ参数的上面:
BogoMIPS: 1593.34
cpu MHZ : 799.58
查看其他机器如redhat,ft等都有这两个参数,只是不放在一起,所认平时很难注意到。
MIPS Google描述为:Million Instructions Per Second,即每秒执行百万(M)指令数。这就奇怪了,当前CPU的主频才799.58MHZ,却可以每秒执行1593.34M个指令。相当每个时钟周期执行两个指令。

时钟周期是CPU主频的倒数,799.58MHZ表示每秒运行799.58百万个时钟周期. 而每执一个计算机指令都需要1至几十个时钟周期。一个指令的执行包括以下步骤:
1)指令预取
2)指令解码
3)执行
4)内存访问
5) 寄存器写回
最后两步是可选的,取决于指令本身。这里每一步都需要至少一个时钟周期来完成。那么一个指令至少需要3个时钟周期来完成? 这里CPU通过流水线技术同时执行不同指令的不同部分,提高CPU的吞吐量。上面的每一步都由CPU的一个功能组件来完成,当指令解码组件执行指令A的指令解码动作时,指令预取组件可以执行指令B的指令预取部分。最理想情况下,一个指令只需要一个时间周期。但仍然不能2倍主频?

部分资料提到指令宽度的概念,即上面所说的每一个功能单元组件有多个,一般3-4个。他们可以同时执行。这样最理想情况,一个指令只需要一个1/3或1/4个时间周期了。如果是这样的话,上面的情况就可以解释了。另外业内经常提到的IPC(或CPI),即每指令周期数,在这里应该就是: CPI = MHZ/MIPS。从上面分析来看,CPI是可以小于1的。这里只是一个合理解释,不一定正确,期待指正。
参考:
https://courses.cs.washington.edu/courses/cse471/07sp/lectures/Lecture3.pdf

面向对象众生相

发表于 2018-11-22 | 更新于 2018-12-21

编程语言层出不穷,面向对象的样式各有特色。各种意料之外的编程模型,惊呼”哦,面象对象还可以这样玩”。其中比较容易理解的是Java和C++的对象模型,两者之间区分度也很小: Java的成员函数只能在类内定义, 而C++的成员函数可以类内声明,类外定义。(当然区分并不止这么点)。

后起之秀Go语言对面向对象有了新的定义: 面向对象就是将要处理的数据跟函数进行绑定的方法。

定义数据类型:

1
2
3
type Dog struct{
name string
}

绑定函数:

1
2
3
func (dog Dog) move() {
fmt.Println(dog.name)
}

调用函数:

1
2
3
4
func main(){
  var aDog Dog
   aDog.move();
}

只需要为函数指定接收者,即完成了数据与函数的绑定,而且接收者可以是基础类型,比如int等。可以做到一切皆对象。自由绑定成员函数,使项目功能扩展极其方便。

起初笔者并不喜欢Javascript面向对象的方法,go的出现再次重申了Javascript面向对象的终极地位。且看Javascript几种创建对象的神技:

方法一:通过Object对象

1
2
3
4
5
var box = new Object();
box.name = 'Lee';
box.getName = function () {
return this.name
};

这种方式与go倒有几分相似,不过Javascript不但能绑定函数,还可以增加数据成员。go语言并无出其右。

方法二: 构造函数式:

1
2
3
4
5
6
7
function Box(name){
this.name = name;
this.getName = function () {
return this.name;
}
}
var box1 = new Box('Lee')

方法三:原型模式

1
2
3
4
5
6
function Box() {
}
Box.prototype.name = 'Lee';
Box.prototype.getName = function () {
return this.name;
};

方法四,五…, 招式只局限于你的想象力。为什么Javascript要搞这么复杂, 搞这么复杂…?

其实是Javascript玩了个七十二变的戏法, 让我们产生了错觉。下面来拆解Javascript这种怪异的OOP玩法?

对于方法一, Object也是一个函数(typeof Object == function), 这个函数是Javascript内置的,不需要程序员重新定义。那它和方法二之间的相同基因就不难理解了。

方法一中Javascript可以为对象本身增加属性和绑定方法。方法二也是一样, 而this就是这个对象, 函数为this对象增加属性和绑定方法。再看方法三的模式, 同方法一、二并无不同, 只不过Box.prototype才是这个对象。

其实Javascript中每一个函数都有一个本尊(原型)对象,函数用这个对象作为创建新对象的基础对象, 我们把这个对象看成是具有变化能力的齐天大圣, 新对象都是大圣变化而来。这个本尊对象就是funcation_name.prototype。

而函数就是这位本尊的变身大法,也就是克隆出新对象的构造器, 启动变身大法(new 函数)本尊就会变成想要的模样。这样方法二就容易理解了。

对于方法三, Box.prototype就是Box函数的本尊,改变Box.prototype:Box.prototype.name = ‘Lee’。相当于改变了函数的本尊(原型)对象(由原来的孙悟空变成猪悟能), 构造器的当然就是在这个新本尊的基础上构建新对象。

更有意思的,方法二可以直接作为普通函数调用: Box(‘Lee’),也可以作为构造函数调用 var box1 = new Box(‘Lee’)。函数里的this代表的对象什么不同呢?作为构造函数时,前面已经说过,这里不再重述。但作为普通函数,this却是当前Window对象,这里相当于window.Box(“Lee”),即它是作为Window的成员函数而存在的。Javascript中所有的全局函数和对象都属于Window对象的属性和方法。

网上有很多Javascript面向对象的方法变体,用此方法,即可解疑惑。

12

路人甲

15 日志
RSS
© 2019 路人甲
由 Hexo 强力驱动 v3.7.1
|
主题 – NexT.Muse v6.4.1