一支人

碎碎念

不同形式的锁

最近发现锁的类型真是多种多样,好多还是第一次见,我就在这里记录一下。

RCU

RCU即read-copy-update.一种不阻塞读线程,只阻塞写线程的同步方式。

写线程如果有多个要自己做好互斥,一个时间只能有一个写线程。写线程严格执行R-C-U三步操作,但在第三步操作完的时候,因为把原来的值给更新掉了,原来旧的值就需要释放,那么持有了原来旧的值的读线程必须全部操作完成才行。这里所说的操作的旧值新值都是指针,只有指针才可以直接的确保原子性。

所以这里有个关键步骤是synchronize_rcu(),位于U之后和释放旧指针之前。synchronize_rcu的底层实现我不懂,它的原理大概是说等待所有cpu都调度一遍,就可以确保旧的读线程都操作完成了。为什么都调度一遍就可以确保都操作完了呢?因为所有的读操作都要求添加以下语句:

1
2
3
4
5
6
rcu_read_lock(); // 禁止抢占
p = rcu_dereference(gp); // rcu_dereference主要是加内存屏障
if (p != NULL) {
do_something_with(p->a, p->b, p->c);
}
rcu_read_unlock(); // 允许抢占
阅读全文 »

微服务架构

注意:本文涉及面大而广,预计完整阅读时间在1个小时,可以挑章节选择性阅读。

简介

什么是微服务,以下定义来自维基百科

1
2
微服务 (Microservices) 是一种软件架构风格,它是以专注于单一责任与功能的小型功能区块 (Small Building Blocks) 为基础,
利用模块化的方式组合出复杂的大型应用程序,各功能区块使用与语言无关 (Language-Independent/Language agnostic) 的 API 集相互通信。

所以微服务是一种架构层面的概念,它是一种通过一套小型服务来开发单个应用的方法,每个服务运行在自己的进程中,并通过轻量级的机制进行通讯, 通常是通过HTTP。

微服务历史

微服务这个概念在2012年开始出现,作为加快应用程序开发进程的一种方法。

随后微服务架构实践开始在各大软件大会上被分享。2014年Martin Fowler发表文章《Microservices》,这是第一篇详细介绍微服务的文章。对微服务进行了定义,并与传统架构进行了对比,阐述了微服务的优势。

到了2015年,微服务已经很流行,凡是新建的项目,不论是初创公司还是老牌大厂,微服务都会是首选的架构。互联网企业是这样,就算是嵌入式环境下,微服务也被引入并且推广。

阅读全文 »

Golang Test Coverage

简介

本文主要是通过一个详细的例子来讲解golang中集成单元测试和系统测试覆盖率的一般方案。

想当初接手一个毛坯房一般的golang项目,几个go文件,一个build.sh,一个makefile,别的没有了。

写完怎么验证对没对?build通过,然后得部署到环境中,自己构造请求来检查返回值。但是请求是pb格式的,根本无法手工构造,要是json格式的还好弄点。于是我得写个专门的测试程序,写完通过命令行把参数传给这个测试程序,让它构造pb格式的请求并发起请求。随后发现问题,修改问题,再部署上去,这简直是低效到令人发指。

我是个懒人,我不光不想写专门的测试程序,我连部署到环境中都不想部署,毕竟部署到机器上并发送详细测试请求这项工作已经由QA来覆盖了,即使很多团队没有QA,这项工作也应该是要集成到持续集成+持续部署的系统中,不需要每开发一个feature就部署到环境中来进行调试。

所以首先我实现了单元测试的集成,从此无需部署无需专门的测试程序就可以测试功能的正确性。随着测试代码量的增加,我希望有个地方可以统计我哪些代码测到了,哪些没测到,于是我集成了单元测试的覆盖率。为了查看单元测试+系统测试的总体的测试覆盖情况,随后我们又集成了系统测试的覆盖率。为了查看每次提交新代码的覆盖率,随后又集成了增量覆盖率。

最终项目实现了完整的持续集成+持续部署+覆盖率集成。

阅读全文 »

Golang netpoller

本文主要记录go是如何处理网络IO的,以及这么做的目的和原理,穿插一部分源码跟踪。同时对比go的线程模型与别的通用线程模型的差别。

网络阻塞

在Go的实现中,所有IO都是阻塞调用的,Go的设计思想是程序员使用阻塞式的接口来编写程序,然后通过goroutine+channel来处理并发。因此所有的IO逻辑都是直来直去的,先xx,再xx, 你不再需要回调,不再需要future,要的仅仅是step by step。这对于代码的可读性是很有帮助的。

go scheduler一文中我们讲述了go如何处理阻塞的系统调用,当goroutine调用阻塞的系统调用时,这个goroutine和物理线程都会一直处于阻塞状态,不能处理别的任务;而当goroutine调用channel阻塞时,goroutine会阻塞而物理线程不会阻塞,会继续执行别的任务。所以如果我们基于操作系统提供的阻塞的IO接口来构建golang的应用,我们就必须为每个处于阻塞读写状态的客户端建立一个线程。当面对高并发的包含大量处于阻塞IO状态的客户端时,将浪费大量的资源。而如果能够像channel那样处理,就可以避免资源浪费。

Go的解决方案是如channel一般在用户层面(程序员层面)保留阻塞的接口,但是在Runtime内部采用非阻塞的异步接口来与操作系统交互。

这里面关键的角色就是netpoller。

阅读全文 »

Go调度器

我们知道Go里面有成千上万coroutine需要调度执行,而这里面起关键作用的就是Go的调度器,那么Go的调度器在哪里呢?因为我们写Go代码的时候从未显式创建过调度器实例。为了了解调度器,我们先来了解下Go的运行时(Runtime)。

为什么要有Runtime

开销上

我们知道操作系统是可以调度线程的,那么我们可不可以直接让操作系统调用go的线程呢。
POSIX线程(POSIX是线程标准,定义了创建和操纵线程的一套API)通常是在已有的进程模型中增加的逻辑扩展,所以线程控制和进程控制很相似。线程也有自己的信号掩码(signal mask), 线程也可以设置CPU亲和性(CPU affinity),也可以放进cgroups中进行资源管理。假如goroutines(go的执行单元)对应线程的话,使用这些特性对线程进行控制管理就增加了开销,因为go程序运行goroutines(go的执行单元)不需要这些特性。这类消耗在goroutine达到比如10,0000个的时候就会很大。所以go需要有个运行时在调度goroutines而不是只是让操作系统调度线程。

垃圾回收上

go包含垃圾回收(GC)的特性,在垃圾回收的时候所有goroutines必须处于暂停的状态,这样go的内存才会处于一种一致的状态. 所以我们必须等待所有线程处于内存一致的状态才能进行垃圾回收。

在没有调度器的时候,线程调度是随操作系统的意的,你不得不试图去等待所有的已经暂停和还没暂停的线程,而且不知道等多久, 暂停后如何让他们保持暂停直到gc结束,也是一个难题。

在有调度器的时候,调度器可以决定只在内存一致的时候才发起调度(即只要有活跃的线程就不执行新的任务),因此当需要执行gc的时候,调度器便决定只在内存一致的时候才发起调度,所以所有线程都无法再次活跃,调度器只需要等待当前活跃的线程暂停即可。后面还会讲到调度器还想办法避免一个活跃的线程长时间不停下来。

需要调度器自然就需要运行调度器的运行时。

基于这两个原因, golang需要一个运行时(Runtime).

或者简单的讲,要想做协程线程调度就要有运行时。要想做垃圾回收就要有运行时。

阅读全文 »

strange golang receiver

对象receiver

receiver是什么呢,一句话来解释的话约等于this指针。
用过c++的同学我们可以来这么来理解。

理解c++中的this指针

1
2
3
4
5
6
7
class Meta {
public:
string getName(){
return this->name;
}
string name;
};

这个类的成员函数getName中调用了this指针,可是this指针没有定义过呢,哪来的呢?

答案是编译器加的,由于可执行文件中并不存在对象这种概念,但是存在函数的概念,所以编译器就必须把对象的调用转成函数的调用。

编译器是这么做的,他把string getName();这个成员函数转换成string getName(Meta* this);

看到了吧,编译器通过增加一个this参数来吧对象传递到成员函数中去,this指针就这么来了。

golang的’this’指针

我们看到c++中this是隐式提供的,golang则选择了显式的提供this指针,提供的形式就是receiver。

1
2
3
func (receiver) funcName(inputParameters...) (outputParameters...){
//
}
阅读全文 »

LVS简介

什么是LVS:

1
LVS是Linux Virtual Server的简写,意即Linux虚拟服务器,是一个虚拟的服务器集群系统。本项目在1998年5月由章文嵩博士成立,是中国国内最早出现的自由软件项目之一.

LVS的作用:

LVS的原理很简单,当用户的请求过来时,会直接分发到LVS机器(director server)上,然后它把用户的请求根据设置好的调度算法,智能均衡地分发到后端真正服务器(real server)上。
简单的讲,LVS就是一种负载均衡服务器。

LVS的角色:

1
2
3
4
5
DS:director server,即负载均衡器,根据一定的负载均衡算法将流量分发到后端的真实服务器上.
RS:real server 真实的提供服务的server,可被DS划分到一个或多个负载均衡组.
BDS:backup director server,为了保证负载均衡器的高可用衍生出的备份.
VS:vitual server,负载均衡集群对外提供的IP+Port.
VIP:VS的IP,client请求服务的DIP(destination IP address),定义在DS上,client或其网关需要有其路由

LVS组成:

LVS 由2部分程序组成,包括ipvs和ipvsadm。
ipvs工作在内核空间,是真正生效实现调度的代码.

1
2
3
4
ipvs基于netfilter框架,netfilter的架构就是在整个网络流程的若干位置放置一些检测点(HOOK).
在每个检测点上登记一些处理函数进行处理(如包过滤,NAT等,甚至可以是用户自定义的功能)。
IPVS就是定义了一系列的“钩子函数”,在INPUT链和Forward上放置一些HOOK点.
如匹配了ipvs的规则,就会通过函数来对数据包进行操作,比如修改目的IP为realserver的接口IP(NAT),对MAC进行修改(DR)等等。

ipvsadm工作在用户空间,负责为ipvs内核框架编写规则, 它是一个工具,通过调用ipvs的接口去定义调度规则,定义虚拟服务(VS)。

LVS请求的流程

1
2
3
4
1、客户端(Client)访问请求发送到调度器(Director Server)。
2、调度器的PREROUTING链会接收到用户请求,判断目标IP确定是本机IP,将数据包发往INPUT链。
3、INPUT链的IPVS会根据ipvsadm定义的规则(调度模式和调度算法等等)进行对比判断。
4、如果用户请求就是所定义的虚拟服务(vitual server),那么IPVS会修改请求包的ip、mac、端口号等信息,并将请求发送到FORWARD链,再经由POSTROUTING链发送到后端的真实提供服务的主机(Real Server)

下面我主要记录一下LVS调度的方式和原理。

阅读全文 »

Lock-Free

什么是Lock-Free

Lock-Free也叫LockLess也就是无锁编程,它是一种在多线程之间安全的共享数据的一种方式,并且不需要有获取和释放锁的开销。但是不使用锁来进行编程却只是无锁编程的一部分。我们先用一个图来看看如何判断是不是无锁编程:
如何确认是lockfree
从这个图上可以看出,无锁编程中的锁并不是直接指向lock或者说mutex,而是指一种把整个程序锁住的可能性,无论是死锁还是活锁,甚至是因为你做了你能想到的最差的线程调度决策。如此一来,即使是共享锁也被排除在外。因为当你用std::shared_lock<std::shared_mutex> lock(mutex_); 拿到锁之后,你可以简单地再也不调度这个线程,来导致 std::unique_lock<std::shared_mutex> lock(mutex_);永远得不到锁。

下面这个例子中我们不使用mutex和lock,但是它依然不是Lock-Free,因为你可以调度执行这个函数的两个线程使得它俩都不退出循环。

1
2
3
4
while (X == 0)
{
X = 1 - X;
}

我们并不会期望整个大程序都是Lock-Free,通常我们会指明其中某些操作是Lock-Free的,比如一个队列的实现中,我们会存在少数的Lock-Free操作,比如push,pop 等等。

Lock-Free的一个重要的结论是,当你暂停一个线程的运行,它并不会阻止其他线程继续执行,其他线程就像一个整体,继续他们的Lock-Free操作。这也是Lock-Free编程的价值,特别是当你编写中断处理程序、实时系统时,他们必须在规定的时间内完成任务,无论程序的其他部分怎么执行。

阅读全文 »

epoll 中level trigger的检验

简介

在我们通过网络搜索的时候,关于epoll的水平触发的解释通常是这样的:

水平触发:只要缓冲区还有数据,内核就还会通知用户。用户如果第一次读取数据没读完,即使没有任何新的操作触发,还是可以继续通过epoll_wait来获取事件。

这段解释水平触发的话应该来说是没有错的,然而这段话并不总是如你所想的,说这句话的人不会告诉你什么场景满足什么场景是不满足的。我就是要说一个你一定以为满足,实际上却不满足的场景。

我们先来看成立的情况:

阅读全文 »

线程扩展要考虑的问题

简介

如今越来越多的后台系统采用了单线程的设计,原因多是因为单线程的设计简单轻量,而且更安全(无多线程竞争)。
而其性能保证则通过多进程的方式来完成,多进程由于互相隔离,也是稳定性的保证。
比如nginx默认工作方式就是单线程多进程;其他的案例比如node.js的集群;比如redis的集群。
通常这种方式能很好的工作,然而有些时候你还是会面临项目向多线程转变的时候,比如项目的技术栈从node.js转向Go,Go又是天生为并发而生的;
比如项目在演进过程中出现了CPU密集型的业务,单线程会严重影响性能;比如多进程架构扩展时遇到了IP等资源不足(比如业务需要为每个进程绑定一个公网IP)的限制。
那么当你在处理这种架构转变的时候,需要考虑哪些问题呢?

阅读全文 »
0%