Golang的Mysql操作

背景

前面我已经写过一篇关于操作Redis的博客了。这一次写Golang如何操作Mysql。

基础:

  1. 安装并会用Golang
  2. 安装并配置了kubernetes
  3. kubernetes中已经运行了mysql服务

例子

在讲Mysql的操作之前先上一个小例子

  1 package main
  2 
  3 import (
  4         "database/sql"
  5         _ "github.com/go-sql-driver/mysql"
  6         "log"
  7 )
  8 
  9 func main() {
 10         db, err := sql.Open("mysql", "root:123456@tcp(192.168.3.52:3306)/mysql?parseTime=true")
 11         if err != nil {
 12                 log.Fatal(err)
 13         }
 14 
 15         _, err1 := db.Exec("CREATE TABLE IF NOT EXISTS mysql.hello(world varchar(50))")
 16         if err1 != nil {
 17                 log.Fatal(err1)
 18         }
 19 
 20         rs, _ := db.Exec("INSERT INTO mysql.hello(world) VALUES ('hello world')")
 21 
 22         rowCount, err3 := rs.RowsAffected()
 23         if err3 != nil {
 24                 log.Fatal(err3)
 25         }
 26 
 27         rs1, _ := db.Query("SELECT world FROM mysql.hello")
 28         for rs1.Next(){
 29           var s string
 30           rs1.Scan(&s)
 31           log.Printf(s)
 32         }
 33 
 34 
 35         log.Printf("insert %d rows", rowCount)
 36 
 37         defer db.Close()
 38 
 39 }

连接

我们这里要使用到Mysql的标准库database/sql与驱动go-sql-driver/mysql。
连接使用标准库中的sql.Open返回数据库对象db,我们接下来就可以用db中的如Exec、Query等方法进行数据库的操作了。
现在来看看sql.Open的参数

sql.Open(Dbuser+":"+DbPasswd+"@tcp("+DbHost+")/"+DbName+"?parseTime=true")

参数就是这样,没什么好解释的,具体例子可以看上面的代码。

操作

操作也比较简单,
db.Exec(SQL操作)可以用来建表,插入数据
db.Query(查询操作)用来查询数据
上述例子中查询操作返回给rs1,然后将rs1中的值遍历出来进行显示,代码如下

for rs1.Next(){
           var s string
           rs1.Scan(&s)
           log.Printf(s)
}

Golang的Redis操作

背景

本篇博客主要讲解在kubernetes集群中,如何使用go语言对Redis与Mysql进行操作。
在此之前要有此基础:

  1. 安装并会使用go语言
  2. 配置好了kubernetes
  3. 在kubernetes中配置好了redis服务

连接

使用go连接Redis需要github.com/garyburd/redigo/redis这个包。这里的redigo是一个go语言的redis客户端实现。redigo没有其它别的依赖项,我们可以直接通过go get来安装它。

go get github.com/garyburd/redigo/redis

但是在安装时我们有可能会碰到如下bug:
这里写图片描述
这是因为我们没有配置GOPATH环境变量的原因。需要按如下命令配置:
这里写图片描述
当然,在命令行中设置环境变量并不是永久生效的。要永久生效,你可以在配置文件中去配置。
安装完成我们就可以进行连接了。
Redis中提供的有现成的方法
c,err := redis.Dial("tcp","192.168.3.3:6379")
Dial方法第一个参数是网络连接的方式,第二个参数是redis服务的入口地址。我们可以使用Node IP+端口的方式,也可以用service IP+端口的方式。我这里演示的是用service IP+端口的方法访问。这适用于在kubernetes集群内部进行访问,若是从外部访问就需要用node IP+端口的方式了。
这里写图片描述
这里我们可以看到redis-master这个service对象的IP是192.168.3.3,端口号为6379。

命令操作

Conn接口是与Redis协作的主要接口,我们刚才已经用Dial方法建立了连接了。现在我们就可以使用Conn借口来对Redis进行操作。
这里举一个简单的例子:

  1 package main
  2 
  3 import (
  4         "fmt"
  5         "github.com/garyburd/redigo/redis"
  6 )
  7 
  8 func main() {
  9         c, _ := redis.Dial("tcp", "192.168.3.3:6379")
 10         _ ,err := c.Do("SET","username","pingfan")
 11         if err != nil{
 12         fmt.Printf("ERROR",err)
 13         }
 14         username, _ := redis.String(c.Do("GET", "username"))
 15         fmt.Println("Got username ",username)
 16  }

9行之前已经讲过了
我们操作Redis,就是用Conn接口的Do方法,Do是一个通用方法,它能做许多事。
比如10与14行,分别表示写与读。
我这里用service IP访问redis-master服务。代码运行结果如下:
这里写图片描述
其它还有许多操作:
批量写入读取

MGET key [key ...]
MSET key value [key value ...]

批量写入读取对象(Hashtable)

HMSET key field value [field value ...]
HMGET key field [field ...]

检测值是否存在

EXISTS key

删除

DEL key [key ...]

设置过期时间

EXPIRE key seconds

搭建docker镜像私有仓库

1

docker官方提供了docker hub来存储我们的镜像文件。但由于docker hub管理的都是公开的镜像,并且访问的时候有时会特别的慢。所以我们打算来搭建一个私有的仓库,需要使用官方库中的registry镜像。

2

下载registry镜像

docker pull registry

运行registry镜像
docker run -d -p 5000:5000 -v /root/pf_registry:/var/lib/registry registry
以上的-d与-p参数在前一篇博客中已经讲过,所以,我们将容器的5000端口映射到宿主机的5000端口。我们一会就可以访问这个端口来访问容器
-v参数是用来将容器的数据绑定到宿主机上的,如此一来,容器中/var/lib/registry的数据就与/root/pf_registry中的数据同步了,做这件事的目的是将容器中存储的镜像文件在宿主机上也能够存在。这样如果容器停止时我们也能够看到镜像文件。

3

这些做好了,我们的仓库也就搭建好了,接下来就可以push我们的镜像文件到私有仓库中了
在push之前,我们需要将镜像的名称改为registry.pf.com:5000/tomcat的格式。其中registry.pf.com是宿主机的IP地址,之所以用registry.pf.com而不用IP地址,是因为我在/etc/hosts中进行了设置,如下:
这里写图片描述
5000是之前映射到的宿主机的端口
tomcat是镜像的名称

命令如下:
docker tag tomcat registry.pf.com:5000/tomcat

4

此时,我们就可以将tomcat镜像文件push到私有仓库中了
docker push registry.pf.com:5000/tomcat
push之后,我们就可以进入我们指定的绑定容器的目录下来查看push成功的镜像文件
如下:
这里写图片描述

1

从docker hub中将tomcat镜像拉下来
这里写图片描述

2

可以看一下详细信息,使用docker pull tomcat拉取tomcat时若不指定标签,则会使用默认的latest标签
这里写图片描述

3

接下来我们要让这个镜像作为容器在后台运行起来,并且将该容器的端口号暴露出来,也就是说,将容器的端口映射到我们自己计算机的物理端口上
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CHgE1WCy-1611660147218)(http://img.blog.csdn.net/20170802180545791?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvaHR5NDY1NjU=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)]
-d参数是让tomcat容器在后台运行
-p参数是将容器的端口映射给宿主机的端口
这里写图片描述
我们可以用docker run --help来看该命令各个参数的意义
注意:这里的16000是宿主机的端口,8080是容器的端口,不可以写反

4

成功执行后,我们就可以通过访问16000端口来看tomcat是否成功运行
这里写图片描述
可以看到,我这里已经成功运行了
或者在命令行中使用curl命令来看是否成功

5

同样也可以使用并测试nginx,但要注意的是nginx容器的端口是80
这里写图片描述

进程地址空间(Linux内核源码分析)

背景

之前写过关于内存管理源码分析的博客。大体介绍了什么是页、区、slab缓存,以及内核获取、释放页的接口,分配、释放slab缓存的接口。进程地址空间简单的说就是用户空间中进程的内存,我们叫这内存为进程地址空间。本篇博客借助linux源码大体分析进程地址空间的相关知识。

进程控制块

既然我们要聊一聊进程地址空间,那么不可避免的就要先聊一下进程控制块,进程控制块的概念想必大家不会陌生。一个进程是由一个进程控制块来描述的。所以,可想而知,进程控制块中一定包含有进程地址空间的描述结构体。这个描述进程地址空间的结构体就是mm_struct。先来看下task_struct结构体。(位于linux/sched.h中)
这里写图片描述
这是截取了task_struct中的一部分代码。

mm_struct

接下来我们就重点的看一下mm_struct结构体。此结构体是用于描述进程地址空间的,所以每个进程都有唯一的mm_struct结构体,即唯一的进程地址空间。我们来看一下此结构体的部分源码(位于linux/mm_types.h中)。
这里写图片描述
203行的mmap域与204行的mm_rb域描述的对象是相同的。为什么要用不同的方式来组织相同的对象呢?原因在于,用mmap作为链表可以方便遍历所有元素;而mm_rb作为红黑树,适合于搜索指定的元素。
215行的mm_users域与216行的mm_count域都是原子数,用于用户引用此进程地址空间(也就是此结构体)的计数。区别在于,mm_users表示的是有多少进程在引用此进程地址空间,若其为3,则表示有3个线程共享此进程地址空间。mm_count表示的是如果有用户使用此进程地址空间,那么此域值为1,否则域为0。当mm_count为0时,此结构体就会被撤销。
所有的mm_struct结构体都通过双向链表mmlist域连接在一起。双向链表的链表头是init进程的进程地址空间。
mm_struct结构体的源码就分析这么多。
我们都知道一个进程有唯一的一个mm_struct结构体,即一个进程有唯一的一个进程地址空间。那么,在创建一个新进程时,mm_struct是如何被创建的?
我之前有一篇博客讲过slab缓存的作用以及用法,提到过slab缓存的作用在于快速为常用的结构体分配空间。很显然mm_struct是一个常用的结构体,新进程的mm_struct结构体是通过allocate_mm()宏(位于linux/fork.h中)从mm_cachep slab缓存中分配得来的。前面提到过,一个进程有唯一的进程地址空间,而线程没有自己的进程地址空间,不同线程共享同一个进程地址空间。是否共享地址空间几乎是进程和Linux中线程间本质上的唯一区别。那么,内核线程与进程地址空间的关系是如何的呢?

内核线程与进程地址空间

内核线程没有进程地址空间,当然也就没有mm_struct结构体。所以内核线程对应的进程描述符中的mm域为NULL。请大家回想一下在一个进程被调度时进程地址空间是如何进行转换的?
实际上是该进程的mm域指向的进程地址空间被装载到内存中,进程描述符中的active_mm域被更新指向新的进程地址空间。
而内核线程是没有进程地址空间的,所以其mm域为NULL,当内核线程被调度时,内核发现它的mm域为NULL,就会保留前一个进程的进程地址空间,更新内核线程进程描述符中的active_mm域指向前一个进程的进程地址空间。因为内核线程不访问用户空间的内存,所以它们仅仅使用地址空间中和内核内存相关的信息,这些信息的含义和普通进程完全相同。
上面讲过的mm_struct结构体是描述进程地址空间的,而通过分析源码我们发现,mm_struct结构体并没有涉及到具体的虚拟内存区域,如内存区间何起何止。但在mm_struct结构体的第一个域mmap指向的vm_area_struct结构体中描述了虚拟内存区域。我们来看看这个结构体。

vm_area_struct

这里写图片描述
这里截取了部分vm_area_struct的源码。(位于linux/mm_types.h中)
其中136行的vm_start域是进程地址空间的起始地址,137行的vm_end域是结束地址。
135行的vm_mm域指向相关的mm_struct结构体。也就是此虚拟内存区域相关的进程地址空间。
聊完虚拟内存区域VMA,接下来就可以聊一下如何为一个进程地址空间创建一个VMA。

do_mmap

内核使用do_mmap函数创建一个新的线性地址空间,由于新创建的地址空间若是与原有的地址空间相邻内核会自动进行合并,所以更严格的来说,do_mmap不一定总是创建新的VMA,而可能是扩展已有的VMA。来看看do_mmap的源码(位于linux/mm.h中)
这里写图片描述
函数参数中的file、offset、len域分别指要映射的文件、要映射的起始偏移地址、映射的长度。
其中file域可以为空,这时指此次映射与文件无关,这叫做匿名映射,如果是与文件有关的,叫做文件映射。
addr是可选参数,它指定搜索空闲区域的起始位置。prot参数指定内存区域中页面的访问权限。flag参数指定了VMA标志。
有do_mmap来创建新的地址空间,当然就有do_munmap来删除地址空间。该函数定义在mm/mmap.c中。这里就不赘述了。

总结

本篇博客先介绍了什么是进程地址空间,然后介绍了其描述符mm_struct,一个进程对应一个进程地址空间,也就是说task_struct对应一个mm_struct。内存描述符mm_struct介绍完后聊了一下其与内核线程的关系,因为我们知道,内核线程是没有进程地址空间的。所以内核线程使用前一个进程的mm_struct。接下来介绍了一下vm_area_struct结构体,它描述了虚拟内存区域,也就是进程地址空间中的内存区域。最后简要说了一下创建线性地址空间的函数do_mmap。

内存管理(Linux内核源码分析)

背景

本篇博客试图通过linux内核源码分析linux的内存管理机制,并且对比内核提供的几个分配内存的接口函数。然后聊下slab层的用法以及接口函数。

内核分配内存与用户态分配内存

内核分配内存与用户态分配内存显然是不同的,内核不可以像用户态那样奢侈的使用内存,内核使用内存一定是谨小慎微的。并且,在用户态如果出现内存溢出因为有内存保护机制,可能只是一个报错或警告,而在内核态若出现内存溢出后果就会严重的多(毕竟再没有管理者了)。

我们知道处理器处理数据的基本单位是字。而内核把页作为内存管理的基本单位。那么,页在内存中是如何描述的?
内核用struct page结构体表示系统中的每一个物理页:
这里写图片描述
flags存放页的状态,如该页是不是脏页。
_count域表示该页的使用计数,如果该页未被使用,就可以在新的分配中使用它。
要注意的是,page结构体描述的是物理页而非逻辑页,描述的是内存页的信息而不是页中数据。
实际上每个物理页面都由一个page结构体来描述,有的人可能会惊讶说那这得需要多少内存呢?我们可以来算一下,若一个struct page占用40字节内存,一个页有8KB,内存大小为4G的话,共有524288个页面,需要刚好20MB的大小来存放结构体。这相对于4G的内存根本九牛一毛。

有些页是有特定用途的。比如内存中有些页是专门用于DMA的。
内核使用区的概念将具有相似特性的页进行分组。区是一种逻辑上的分组的概念,而没有物理上的意义。
区的实际使用和分布是与体系结构相关的。在x86体系结构中主要分为3个区:ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM。
ZONE_DMA区中的页用来进行DMA时使用。ZONE_HIGHMEM是高端内存,其中的页不能永久的映射到内核地址空间,也就是说,没有虚拟地址。剩余的内存就属于ZONE_NORMAL区。
我们可以看一下描述区的结构体struct zone(在linux/mmzone.h中定义)。
这里写图片描述
这个结构体比较长,我只截取了一部分出来。
实际上不是所有的体系结构都定义了全部区,有些64位的体系结构,比如Intel的x86-64体系结构可以映射和处理64位的内存空间,所以其没有ZONE_HIGHMEM区。而有些体系结构中的所有地址都可用于DMA,所以这些体系结构就没有ZONE_DMA区。

内核中内存分配接口

我们现在已经大体了解了内核中的页与区的概念及描述。接下来我们就可以来看看内核中有哪些内存分配与释放的接口。在内核中,我们正是通过这些接口来分配与释放内存的。首先我们来看看以页为单位进行分配的接口函数。

获得页与释放页

获得页

获得页使用的接口是alloc_pages函数,我们来看下它的源码(位于linux/gfp.h中)
这里写图片描述
可以看到,该函数返回值是指向page结构体的指针,参数gfp_mask是一个标志,简单来讲就是获得页所使用的行为方式。order参数规定分配多少页面,该函数分配2的order次方个连续的物理页面。返回的指针指向的是第一page页面。
获得页的方式不只一种,我们还可以使用__get_free_pages函数来获得页,该函数和alloc_pages的参数一样,然而它会返回一个虚拟地址。源码如下:
这里写图片描述
可以看到,这个函数其实也是调用了alloc_pages函数,只不过在获得了struct page结构体后使用page_address函数获得了虚拟地址。
另外还有alloc_page函数与__get_free_page函数,都是获得一个页,其实就是将前面两个函数的order分别置为了0而已。这里不赘述了。

我们在使用这些接口获取页的时候可能会面对一个问题,我们获得的这些页若是给用户态用,虽然这些页中的数据都是随机产生的垃圾数据,不过,虽然概率很低,但是也有可能会包含某些敏感信息。所以,更谨慎些,我们可以将获得的页都填充为0。这会用到get_zeroed_page函数。看下它的源码:
这里写图片描述
这个函数也用到了__get_free_pages函数。只是加了一种叫做__GFP_ZERO的gfp_mask方式。所以,这些获得页的函数最终调用的都是alloc_pages函数。alloc_pages函数是获得页的核心函数。

释放页

当我们不再需要某些页时可以使用下面的函数释放它们:
__free_pages(struct page *page, unsigned int order)
__free_page
free_pages
free_page(unsigned long addr, unsigned int order)
这些接口都在linux/gfp.h中。
释放页的时候一定要小心谨慎,内核中操作不同于在用户态,若是将地址写错,或是order写错,那么都可能会导致系统的崩溃。若是在用户态进行非法操作,内核作为管理者还会阻止并发出警告,而内核是完全信赖自己的,若是在内核态中有非法操作,那么内核可能会挂掉的。

kmalloc与vmalloc

前面讲的那些接口都是以页为单位进行内存分配与释放的。而在实际中内核需要的内存不一定是整个页,可能只是以字节为单位的一片区域。这两个函数就是实现这样的目的。不同之处在于,kmalloc分配的是虚拟地址连续,物理地址也连续的一片区域,vmalloc分配的是虚拟地址连续,物理地址不一定连续的一片区域。这里依然需要特别注意的就是使用释放内存的函数kfree与vfree时一定要注意准确释放,否则会发生不可预测的严重后果。

slab层

分配和释放数据结构是内核中的基本操作。有些多次会用到的数据结构如果频繁分配内存必然导致效率低下。slab层就是用于解决频繁分配和释放数据结构的问题。为便于理解slab层的层次结构,请看下图
这里写图片描述
简单的说,物理内存中有多个高速缓存,每个高速缓存都是一个结构体类型,一个高速缓存中会有一个或多个slab,slab通常为一页,其中存放着数据结构类型的实例化对象。
分配高速缓存的接口是struct kmem_cache *kmem_cache_create (const char *name, size_t size, size_t align,unsigned long flags, void (*ctor)(void *))。
它返回的是kmem_cache结构体。第一个参数是缓存的名字,第二个参数是高速缓存中每个对象的大小,第三个参数是slab内第一个对象的偏移量。剩下的就不细说。
总之,这个接口函数为一个结构体分配了高速缓存,那么高速缓存有了,是不是就要为缓存中分配实例化的对象呢?这个接口是
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)
参数是kmem_cache结构体,也就是分配好的高速缓存,flags是标志位。
抽象的介绍看着不直观, 我们看个具体的例子。之前我写过一个关于jbd2日志系统的博客,介绍过jbd2的模块初始化过程。其中就提到过jbd2在进行模块初始化的时候是会创建几个高速缓冲区的。如下:
这里写图片描述
我们看看第一个创建缓冲区的函数。
这里写图片描述
首先是断言缓冲区一定为空的。然后用kmem_cache_create创建了两个缓冲区。两个高速缓冲区就这么创建好了。看下图
这里写图片描述
这里用kmem_cache结构体,也就是jbd2_revoke_record_cache高速缓存实例化了一个对象。

总结

内存管理的linux内核源码我只分析了一小部分,主要是总结了一下内核分配与回收内存的接口函数及其用法。

块IO层(Linux内核源码分析)

背景

本篇博客重点分析块IO层,试图了解在linux源码中是如何实现块IO的。

基本知识

块设备与字符设备

块设备与字符设备都是物理外设。简单来说,块设备与字符设备的最大区别在于块设备都随机对数据片段进行读写的,而字符设备都以顺序对数据片段进行读写的。
比如磁盘、CD-ROM盘、闪存就属于块设备。键盘、串口属于字符设备。

扇区与块

扇区是块设备的最小寻址单元,也就是说,是物理上的最小单元。而块则不同,块是文件系统进行IO的最小单元,就是说,块是逻辑上的最小单元。所以,对于编程者来说,可能更为在意的是块的概念。
由此看来,扇区与块的关系就很明了了,块一定是扇区整数倍,而且一定要小于内存中一个页的长度。通过扇区的大小是512字节。

页与块

页是内存中的一个基本管理单元。
块是文件系统的一种抽象,内核执行的所有磁盘操作都是按照块进行的。

源码分析

我们可以先思考一下这个问题,从磁盘这样的块设备中要读取数据到内存中,从整体来看内核需要干什么?
首先是不是需要知道磁盘哪块的数据要放入到内存中的哪个位置?
然后需要知道是如何将数据放入内存中,也就是以怎样的方式?
知道了前面的之后,就需要考虑一个问题,内核中需要处理的块IO一定不只一个,如何在一小段时间内来了许多需要进行块IO的请求,这个时候该怎么处理呢?难道不需要管理随便的找块IO请求进行处理吗?
接下来我们就通过源码逐个分析这些问题。

buffer和buffer_head

从磁盘到内存,我们首先需要知道它们的对应关系是什么,前面基础里提到,内存的基本管理单元是页,磁盘的基本管理单元是块(逻辑上来说)。所以说的具体点就是,哪个块对应哪个页。
当一个块的数据被调入内存中,会有一个缓冲区与之对应,这个缓冲区就相当于块在内存中的表示。一个缓冲区就是buffer。buffer_head结构体就是用来描述它们之间对应关系的。
接下来我们就直接看看这个结构体(位于include/linux/buffer_head中)
这里写图片描述
70的b_bdev指向具体的块设备。
66中的b_blocknr指向逻辑块号。我们就知道了块是哪个块。
64的b_page指向缓冲区位于哪个页面之中。
68的b_data指向页面中的缓冲区开始的位置。
67的b_size显示了缓冲区的大小是多少。
从这几个域我们就知道了磁盘与内存的映射关系了。在内存页中,数据起始于b_data,到b_data+b_size。
其它还有一些域,如b_state显示了buffer的状态。b_count表示了这个buffer_head的被用次数,若被用了就需要给它进行原子加1操作,这样其它地方就不能再重复使用了。
显然,只知道内存与磁盘数据的对应关系还不行,我们还需要知道在内存中的具体的区域,也就是需要知道数据的容器。这个容器就是结构体bio。

bio结构体

我们先来直接看看bio结构体的源码。
这里写图片描述
95行bi_io_vec指向了一个bio_vec结构体数组。这个bio_vec结构体是一个比较关键的东西。我们看一下这个结构体。
这里写图片描述
此结构体中的page大家应该比较熟悉,这个内存中的一个页面。bv_offset表示页中数据的偏移量,bv_len表示这个数据的长度。这下我们应该明白了。bio结构体拥有bio_vec结构体数组,每个结构体包含了一个页面中的一些“片段”。而这“片段”就是之前buffer_head描述的磁盘与内存中对应的块的连续数据。换句话说,“片段”由连续的内存缓冲区组成。
现在再回过头来看bio结构体。
既然知道了bi_io_vec指向bio_vec结构体数组的首地址。那么肯定就得知道数组的长度与当前正操作的bio_vec的索引。
这就是72行bi_vcnt域和73行bi_idx域。bi_vcnt记录数组的数量,bi_idx记录当前bi_vec结构体的索引数。
至此,磁盘与内存的映射关系的结构体我们了解了,块在内存中的数据容器的结构体我们也了解了。现在有个问题,若内核中有多个块IO请求,那么这些IO请求就按照先来先处理的方式来进行吗?很显然这样不合适,我们需要用一些IO调度程序来管理这些IO请求。具体的IO调度程序这里就暂时先不讲了。以后有时间再分析具体的IO调度程序。

Tomcat集群同步原理

概述

随着C/S架构中,客户端对服务器的访问量及访问次数逐渐增多,单个服务器已经不能够满足客户端的请求了。于是现在大多数服务器都做成了集群的形式。而服务器集群会有一个很大问题,就是同步问题。比如,现在我对一个有四台计算机的集群进行访问,这时假设根据负载均衡分配到了Node1,如果我在Node1上创建了一个session对象,这时,在服务器响应客户端之前,一定是要先将创建session对象的信息同步到其它节点上的。这样,我们在客户端第二次发起请求时,假设分到了Node2,我们也可以直接获取session信息。照常进行会话。如果我们在某个服务器上删除了会话,那么同样,在响应之前也会同步其它节点也删除会话。如图:
这里写图片描述
tomcat集群同步的大致过程就是如上所述。那么更深一点的原理是什么呢,接下来我一点一点的深入探索。

同步组件

在上述无论是发送还是接收信息的过程中,使用到的组件主要有三个:Manager,Cluster,tribes。简单来讲,Manager的作用是将操作的信息记录下来,然后序列化后交给Cluster,接着Cluster是依赖于tribes将信息发送出去的。其余节点收到信息后,按照相反的流程一步步传到Manager,经过反序列化之后使该节点同步传递过来的操作信息。如图,假设我们访问的是中间的节点,该节点将信息同步出去。信息是以Cluster Message对象发送的。
这里写图片描述

同步方式

关于集群的具体同步机制,tomcat共提供了两种。一种是集群增量会话管理器,另一种是集群备份会话管理器。

集群增量会话管理器

这是一种全节点复制模式,全节点复制指的是集群中一个节点发生改变后会同步到其余全部节点。那么非全节点复制,顾名思义,指的是集群中一个节点发生改变后,只同步到其余一个或部分节点。
除了这一特点,集群增量会话管理器还具有只同步会话增量的特点,增量是以一个完整请求为周期,也就是说会在一个请求被响应之前同步到其余节点上。

集群备份会话管理器

全节点复制模式存在的一个很大的问题就是用于备份的网络流量会随着节点数的增加而急速增加,这也就是无法构建较大规模集群的原因。为了解决这个问题,tomcat提出了集群备份会话管理器。每个会话只有一个备份。这样就可构建大规模的集群。

源码分析

我这里以集群增量会话管理器为例对tomcat7.0.78中的源码进行分析。

DeltaRequest

DeltaRequest对象记录了请求执行过程中的一系列操作。该对象最终会被序列化,然后传输到其余节点后再被反序列化为该对象,从而进行本地节点对会话操作的同步。
这里写图片描述
DeltaRequest对象是记录对会话操作的,那么会话事件(如创建会话,销毁会话,更改会话属性)是在哪里定义的呢?而针对不同会话事件的不同操作是如何定义的呢?

SessionMessageImpl

tomcat中的SessionMessageImpl类定义了不同的会话事件及操作方法。此类与其它类之间的继承(与其它接口的实现)关系如图:
这里写图片描述
有了这样的关系,我们就知道此类继承(实现)了许多属性和方法,比如它实现了SessionMessage接口中对会话事件的定义:
这里写图片描述
它还继承了ClusterMessage类,Serializable类,分别用于对集群的操作和序列化。

ChannelListener

当信息被序列化发送出去后,节点通过信道监听信息。实现的接口如下:
这里写图片描述
这个接口由Cluster具体实现,在tribes接收到message后。首先会调用accept方法判断是否需要接收此信息,如果返回值为True,那么就调用messageReceived方法来接收message。接着它会回调DeltaManager类中的messageDataReceived方法来进行处理。
这里写图片描述
这个方法对各种不同的会话事件进行处理。其中的messageReceived方法通过判断不同的会话事件进行不同的处理。
这里写图片描述

总结

本篇博客只是对于tomcat集群同步原理进行了非常粗线条的源码分析,其中也不一定都对,而且有许多细节没有深究,以后会继续深入分析。

Python爬虫——校园网自动重连脚本

一、背景

最近学校校园网不知道是什么情况,总出现掉线的情况。每次掉线都需要我手动打开web浏览器重新进行账号密码输入,重新进行登录。系统的问题我没办法解决,但是可以写一个简单的python脚本用于自动登录校园网。每次掉线后,再打开任意网页就是这个页面。
这里写图片描述

二、实现代码

#-*- coding:utf-8 -*-
__author__ = 'pf'

import time
import requests

class Login:

    #初始化
    def __init__(self):
        #检测间隔时间,单位为秒
        self.every = 10

    #模拟登录
    def login(self):
        print self.getCurrentTime(), u"拼命连网中..."

        url="http://222.24.19.190:8080/portal/pws?t=li"
        #消息头
        headers={
        'Host':"222.24.19.190:8080",
        'User-Agent':"Mozilla/5.0 (Windows NT 6.3; WOW64; rv:53.0) Gecko/20100101 Firefox/53.0",
        'Accept':"application/json, text/javascript, */*; q=0.01",
        'Accept-Language':"zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3",
        'Accept-Encoding':"gzip, deflate",
        'Referer':"http://222.24.19.190:8080/portal/index_default.jsp",
        'Content-Type':"application/x-www-form-urlencoded",
        'X-Requested-With':"XMLHttpRequest",
        'Content-Length':"291",
        'Connection':"close"
        }
        #提交的信息
        payload={
        'userName':'1403810041',
        'userPwd':'MTk4NDEy',
        'userurl':'http%3A%2F%2Fwww.msn.com%3Focid%3Dwispr&userip=222.24.52.200',
        'portalProxyIP':'222.24.19.190',
        'portalProxyPort':'50200',
        'dcPwdNeedEncrypt':'1',
        'assignIpType':'0',
        'appRootUrl':'=http%3A%2F%2F222.24.19.190%3A8080%2Fportal%2F',
        'manualUrlEncryptKey':'rTCZGLy2wJkfobFEj0JF8A%3D%3D'
        }
        try:
            r=requests.post(url,headers=headers,data=payload)
            print self.getCurrentTime(),u'连上了...现在开始看连接是否正常'
        except:
            print("error")
    #判断当前是否可以连网
    def canConnect(self):
        try:
            q=requests.get("http://www.baidu.com")
            if(q.status_code==200):
                return True
            else:
                return False
        except:
            print 'error'

    #获取当前时间
    def getCurrentTime(self):
        return time.strftime('[%Y-%m-%d %H:%M:%S]',time.localtime(time.time()))

    #主函数
    def main(self):
        print self.getCurrentTime(), u"Hi,欢迎使用自动登陆系统"
        while True:
            self.login()
            while True:
                can_connect = self.canConnect()
                if not can_connect:
                    print self.getCurrentTime(),u"断网了..."
                    self.login()
                else:
                    print self.getCurrentTime(), u"一切正常..."
                time.sleep(self.every)
            time.sleep(self.every)

login = Login()
login.main()

三、解决步骤

  1. 首先需要一个用于抓包的工具。我们要抓取提交的数据以及提交到的url地址。我这里用的是firefox浏览器的httpfox插件。
    这里写图片描述
  2. 用firefox浏览器打开登录页面,并且打开httpfox插件。在页面中输入账户名和密码点击上线后,注意一下httpfox中有一行记录的Method是POST。我们需要记录的就是其中的POST Data中的userName和userPwd。以及Headers中的数据。还有POST到的URL地址。如图:
    这里写图片描述
    这里写图片描述
  3. 我这里使用了python中的requests库。
  4. 将获取到的URL地址、userName、userPwd、Headers填入代码中对应的位置。
    这里写图片描述
  5. 可以直接运行python程序,如图:
    这里写图片描述
  6. 或者可以用pyinstaller库生成exe文件再运行,如图:
    这里写图片描述
    这里写图片描述

四、总结

我这里设置了一个死循环,让程序每隔10s检测一下是否能连上网,若可以连上则输出“一切正常”然后接着循环,若不能连上,则输出“断网了”然后重新连网。我们可以对程序设置开机自启动。这样,开机也就不需要再手动去连网了。

ext4日志系统分析(二)

一、背景

上篇博客介绍了jbd2模块的安装与卸载。同时也讲了安装时初始化函数会在/proc/fs下创建一个jbd2目录。这篇博客打算介绍一下在jbd2模块被安装后内核是如何对日志系统进行初始化的,以及jbd2目录下的文件是从哪里来的。这里所涉及的东西较多,所以我只挑重点功能函数来讲。

二、jbd2目录

不知道大家还记得吗,上一篇博客我提到在安装jbd2模块时,内核会在/proc/fs下创建一个jbd2目录。这个目录具体做什么用到我并没有讲到。现在我们来看一下这个目录中有什么东西。
这里写图片描述
这个sda1-8目录表示日志存储在这个目录中,它位于分区sda1的inode号为8的文件中。进到该目录中。
这里写图片描述
这里有个info文件。打开看看内容。
这里写图片描述
这个文件中都是关于日志的基本信息。我们现在就通过源码来分析下这些信息从何而来。

三、源码分析

我们首先来看一下ext4_load_journal函数:
这里写图片描述
这里写图片描述
这个函数是在ext4_fill_super函数中被调用的。这两个函数都位于fs/ext4/super.c文件中。我们知道,这个文件中都是与超级块相关的函数,ext4_fill_super是文件系统为每个分区初始化super_block时调用的。而ext4_load_journal函数则是用来装载日志系统的。这个函数的重点我用红线标出。分别是初始化日志目录与初始化日志分区。
这是由于ext4文件系统可以使用目录作为日志,也可以使用一个分区来作为目录。一般的个人电脑linux都是用目录作为日志的。比如我的linux:
这里写图片描述
jbd2目录下有一个目录sda1-8。指的是日志存储于sda1分区的inode为8的文件中。如果没有inode号,指的是该文件系统用一个分区来作为日志。
由于我的linux中的日志是由在目录中记录的,那么我就来看看第一个函数(函数定义在fs/ext4/super.c中):
这里写图片描述
这里大部分的代码都是在检查错误,比如inode是否在磁盘上,是否可用。重点在于被红线标出的部分,此函数用于初始化日志inode。我们来看看这个函数(函数被定义在fs/jbd2/journal.c中):
这里写图片描述
我看到了一个熟悉的函数jbd2_stats_proc_init,顾名思义,这个函数应该就是用来初始化jbd2目录中的文件的。进去看看(该函数被定义在fs/jbd2/journal.c中):
这里写图片描述
可以看到,这里info被创建,且是一个只读文件。我们再进被标出的结构体。
这里写图片描述
可以看到,这里定义了对文件的操作函数。由于我们主要用的是打开和读取文件操作。所以我们可以进我们感兴趣的函数中一窥。这里看一下jbd2_seq_info_open函数。
这里写图片描述
然后看一下打开操作的结构体。
这里写图片描述
这里重点在被红线标出的函数。进去看一下。
这里写图片描述
此函数把日志的统计信息写到打开文件的private_data指向的seq_file里。
上面的这些结构体和函数都是在fs/jbd2/journal.c中。
看完了打开操作,大家可以自己再看一下读操作。我这里就不展示了。
上面我在各函数间跳的比较多,可能大家看的眼花缭乱的。我这里制作一张图来展示一下上面介绍的这些函数之间的调用关系。
这里写图片描述
我这篇博客讲的是左边的这条线。右边的是分区作为日志的路线。当然我这里讲的比较粗线条,只是大概讲了一下info中的信息是从哪里来的。更细的东西还得继续深入分析源码才行。

四、总结

本篇博客主要分析了jbd2目录下的文件信息的来源。